Merge pull request #60 from RFnexus/interfaces-update
Add additional interfaces and interface options
This commit is contained in:
299
meshchat.py
299
meshchat.py
@@ -26,6 +26,7 @@ from src.backend.announce_handler import AnnounceHandler
|
||||
from src.backend.async_utils import AsyncUtils
|
||||
from src.backend.colour_utils import ColourUtils
|
||||
from src.backend.interface_config_parser import InterfaceConfigParser
|
||||
from src.backend.interface_editor import InterfaceEditor
|
||||
from src.backend.lxmf_message_fields import LxmfImageField, LxmfFileAttachmentsField, LxmfFileAttachment, LxmfAudioField
|
||||
from src.backend.audio_call_manager import AudioCall, AudioCallManager
|
||||
from src.backend.sideband_commands import SidebandCommands
|
||||
@@ -329,8 +330,29 @@ class ReticulumMeshChat:
|
||||
if "interfaces" in self.reticulum.config:
|
||||
interfaces = self.reticulum.config["interfaces"]
|
||||
|
||||
processed_interfaces = {}
|
||||
for interface_name, interface in interfaces.items():
|
||||
interface_data = interface.copy()
|
||||
|
||||
# handle sub-interfaces for RNodeMultiInterface
|
||||
if interface_data.get("type") == "RNodeMultiInterface":
|
||||
sub_interfaces = []
|
||||
for sub_name, sub_config in interface_data.items():
|
||||
if sub_name not in {"type", "port", "interface_enabled", "selected_interface_mode", "configured_bitrate"}:
|
||||
if isinstance(sub_config, dict):
|
||||
sub_config["name"] = sub_name
|
||||
sub_interfaces.append(sub_config)
|
||||
|
||||
# add sub-interfaces to the main interface data
|
||||
interface_data["sub_interfaces"] = sub_interfaces
|
||||
|
||||
for sub in sub_interfaces:
|
||||
del interface_data[sub["name"]]
|
||||
|
||||
processed_interfaces[interface_name] = interface_data
|
||||
|
||||
return web.json_response({
|
||||
"interfaces": interfaces,
|
||||
"interfaces": processed_interfaces,
|
||||
})
|
||||
|
||||
# enable reticulum interface
|
||||
@@ -443,94 +465,178 @@ class ReticulumMeshChat:
|
||||
if "enabled" not in interface_details and "interface_enabled" not in interface_details:
|
||||
interface_details["interface_enabled"] = "true"
|
||||
|
||||
# handle tcp client interface
|
||||
# handle AutoInterface
|
||||
if interface_type == "AutoInterface":
|
||||
|
||||
# set optional AutoInterface options
|
||||
InterfaceEditor.update_value(interface_details, data, "group_id")
|
||||
InterfaceEditor.update_value(interface_details, data, "multicast_address_type")
|
||||
InterfaceEditor.update_value(interface_details, data, "devices")
|
||||
InterfaceEditor.update_value(interface_details, data, "ignored_devices")
|
||||
InterfaceEditor.update_value(interface_details, data, "discovery_scope")
|
||||
InterfaceEditor.update_value(interface_details, data, "discovery_port")
|
||||
InterfaceEditor.update_value(interface_details, data, "data_port")
|
||||
|
||||
# handle TCPClientInterface
|
||||
if interface_type == "TCPClientInterface":
|
||||
|
||||
interface_target_host = data.get('target_host')
|
||||
interface_target_port = data.get('target_port')
|
||||
|
||||
# ensure target host provided
|
||||
interface_target_host = data.get('target_host')
|
||||
if interface_target_host is None or interface_target_host == "":
|
||||
return web.json_response({
|
||||
"message": "Target Host is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure target port provided
|
||||
interface_target_port = data.get('target_port')
|
||||
if interface_target_port is None or interface_target_port == "":
|
||||
return web.json_response({
|
||||
"message": "Target Port is required",
|
||||
}, status=422)
|
||||
|
||||
interface_details["target_host"] = data.get('target_host')
|
||||
interface_details["target_port"] = data.get('target_port')
|
||||
# set required TCPClientInterface options
|
||||
interface_details["target_host"] = interface_target_host
|
||||
interface_details["target_port"] = interface_target_port
|
||||
|
||||
# set optional TCPClientInterface options
|
||||
InterfaceEditor.update_value(interface_details, data, "kiss_framing")
|
||||
InterfaceEditor.update_value(interface_details, data, "i2p_tunneled")
|
||||
|
||||
# handle I2P interface
|
||||
if interface_type == "I2PInterface":
|
||||
interface_details['connectable'] = "True"
|
||||
interface_details["peers"] = data.get('peers')
|
||||
|
||||
# handle tcp server interface
|
||||
if interface_type == "TCPServerInterface":
|
||||
|
||||
interface_listen_ip = data.get('listen_ip')
|
||||
interface_listen_port = data.get('listen_port')
|
||||
|
||||
# ensure listen ip provided
|
||||
interface_listen_ip = data.get('listen_ip')
|
||||
if interface_listen_ip is None or interface_listen_ip == "":
|
||||
return web.json_response({
|
||||
"message": "Listen IP is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure listen port provided
|
||||
interface_listen_port = data.get('listen_port')
|
||||
if interface_listen_port is None or interface_listen_port == "":
|
||||
return web.json_response({
|
||||
"message": "Listen Port is required",
|
||||
}, status=422)
|
||||
|
||||
interface_details["listen_ip"] = data.get('listen_ip')
|
||||
interface_details["listen_port"] = data.get('listen_port')
|
||||
# set required TCPServerInterface options
|
||||
interface_details["listen_ip"] = interface_listen_ip
|
||||
interface_details["listen_port"] = interface_listen_port
|
||||
|
||||
# set optional TCPServerInterface options
|
||||
InterfaceEditor.update_value(interface_details, data, "device")
|
||||
InterfaceEditor.update_value(interface_details, data, "prefer_ipv6")
|
||||
|
||||
# handle udp interface
|
||||
if interface_type == "UDPInterface":
|
||||
|
||||
interface_listen_ip = data.get('listen_ip')
|
||||
interface_listen_port = data.get('listen_port')
|
||||
interface_forward_ip = data.get('forward_ip')
|
||||
interface_forward_port = data.get('forward_port')
|
||||
|
||||
# ensure listen ip provided
|
||||
interface_listen_ip = data.get('listen_ip')
|
||||
if interface_listen_ip is None or interface_listen_ip == "":
|
||||
return web.json_response({
|
||||
"message": "Listen IP is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure listen port provided
|
||||
interface_listen_port = data.get('listen_port')
|
||||
if interface_listen_port is None or interface_listen_port == "":
|
||||
return web.json_response({
|
||||
"message": "Listen Port is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure forward ip provided
|
||||
interface_forward_ip = data.get('forward_ip')
|
||||
if interface_forward_ip is None or interface_forward_ip == "":
|
||||
return web.json_response({
|
||||
"message": "Forward IP is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure forward port provided
|
||||
interface_forward_port = data.get('forward_port')
|
||||
if interface_forward_port is None or interface_forward_port == "":
|
||||
return web.json_response({
|
||||
"message": "Forward Port is required",
|
||||
}, status=422)
|
||||
|
||||
interface_details["listen_ip"] = data.get('listen_ip')
|
||||
interface_details["listen_port"] = data.get('listen_port')
|
||||
interface_details["forward_ip"] = data.get('forward_ip')
|
||||
interface_details["forward_port"] = data.get('forward_port')
|
||||
# set required UDPInterface options
|
||||
interface_details["listen_ip"] = interface_listen_ip
|
||||
interface_details["listen_port"] = interface_listen_port
|
||||
interface_details["forward_ip"] = interface_forward_ip
|
||||
interface_details["forward_port"] = interface_forward_port
|
||||
|
||||
# handle rnode interface
|
||||
# set optional UDPInterface options
|
||||
InterfaceEditor.update_value(interface_details, data, "device")
|
||||
|
||||
# handle RNodeInterface
|
||||
if interface_type == "RNodeInterface":
|
||||
|
||||
# ensure port provided
|
||||
interface_port = data.get('port')
|
||||
if interface_port is None or interface_port == "":
|
||||
return web.json_response({
|
||||
"message": "Port is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure frequency provided
|
||||
interface_frequency = data.get('frequency')
|
||||
if interface_frequency is None or interface_frequency == "":
|
||||
return web.json_response({
|
||||
"message": "Frequency is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure bandwidth provided
|
||||
interface_bandwidth = data.get('bandwidth')
|
||||
if interface_bandwidth is None or interface_bandwidth == "":
|
||||
return web.json_response({
|
||||
"message": "Bandwidth is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure txpower provided
|
||||
interface_txpower = data.get('txpower')
|
||||
if interface_txpower is None or interface_txpower == "":
|
||||
return web.json_response({
|
||||
"message": "TX power is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure spreading factor provided
|
||||
interface_spreadingfactor = data.get('spreadingfactor')
|
||||
if interface_spreadingfactor is None or interface_spreadingfactor == "":
|
||||
return web.json_response({
|
||||
"message": "Spreading Factor is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure coding rate provided
|
||||
interface_codingrate = data.get('codingrate')
|
||||
if interface_codingrate is None or interface_codingrate == "":
|
||||
return web.json_response({
|
||||
"message": "Coding Rate is required",
|
||||
}, status=422)
|
||||
|
||||
# set required RNodeInterface options
|
||||
interface_details["port"] = interface_port
|
||||
interface_details["frequency"] = interface_frequency
|
||||
interface_details["bandwidth"] = interface_bandwidth
|
||||
interface_details["txpower"] = interface_txpower
|
||||
interface_details["spreadingfactor"] = interface_spreadingfactor
|
||||
interface_details["codingrate"] = interface_codingrate
|
||||
|
||||
# set optional RNodeInterface options
|
||||
InterfaceEditor.update_value(interface_details, data, "callsign")
|
||||
InterfaceEditor.update_value(interface_details, data, "id_interval")
|
||||
InterfaceEditor.update_value(interface_details, data, "airtime_limit_long")
|
||||
InterfaceEditor.update_value(interface_details, data, "airtime_limit_short")
|
||||
|
||||
# handle RNodeMultiInterface
|
||||
if interface_type == "RNodeMultiInterface":
|
||||
|
||||
# required settings
|
||||
interface_port = data.get("port")
|
||||
sub_interfaces = data.get("sub_interfaces", [])
|
||||
|
||||
# ensure port provided
|
||||
if interface_port is None or interface_port == "":
|
||||
@@ -538,42 +644,114 @@ class ReticulumMeshChat:
|
||||
"message": "Port is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure frequency provided
|
||||
if interface_frequency is None or interface_frequency == "":
|
||||
# ensure sub interfaces provided
|
||||
if not isinstance(sub_interfaces, list) or not sub_interfaces:
|
||||
return web.json_response({
|
||||
"message": "Frequency is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure bandwidth provided
|
||||
if interface_bandwidth is None or interface_bandwidth == "":
|
||||
return web.json_response({
|
||||
"message": "Bandwidth is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure txpower provided
|
||||
if interface_txpower is None or interface_txpower == "":
|
||||
return web.json_response({
|
||||
"message": "TX power is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure spreading factor provided
|
||||
if interface_spreadingfactor is None or interface_spreadingfactor == "":
|
||||
return web.json_response({
|
||||
"message": "Spreading Factor is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure coding rate provided
|
||||
if interface_codingrate is None or interface_codingrate == "":
|
||||
return web.json_response({
|
||||
"message": "Coding Rate is required",
|
||||
"message": "At least one sub-interface is required",
|
||||
}, status=422)
|
||||
|
||||
# set required RNodeMultiInterface options
|
||||
interface_details["port"] = interface_port
|
||||
interface_details["frequency"] = interface_frequency
|
||||
interface_details["bandwidth"] = interface_bandwidth
|
||||
interface_details["txpower"] = interface_txpower
|
||||
interface_details["spreadingfactor"] = interface_spreadingfactor
|
||||
interface_details["codingrate"] = interface_codingrate
|
||||
|
||||
# remove any existing sub interfaces, which can be found by finding keys that contain a dict value
|
||||
# this allows us to replace all sub interfaces with the ones we are about to add, while also ensuring
|
||||
# that we do not remove any existing config values from the main interface config
|
||||
for key in interface_details:
|
||||
value = interface_details[key]
|
||||
if isinstance(value, dict):
|
||||
del interface_details[key]
|
||||
|
||||
# process each provided sub interface
|
||||
for idx, sub_interface in enumerate(sub_interfaces):
|
||||
|
||||
# ensure required fields for sub-interface provided
|
||||
missing_fields = []
|
||||
required_subinterface_fields = ["name", "frequency", "bandwidth", "txpower", "spreadingfactor", "codingrate", "vport"]
|
||||
for field in required_subinterface_fields:
|
||||
if field not in sub_interface or sub_interface.get(field) is None or sub_interface.get(field) == "":
|
||||
missing_fields.append(field)
|
||||
if missing_fields:
|
||||
return web.json_response({
|
||||
"message": f"Sub-interface {idx + 1} is missing required field(s): {', '.join(missing_fields)}"
|
||||
}, status=422)
|
||||
|
||||
sub_interface_name = sub_interface.get("name")
|
||||
interface_details[sub_interface_name] = {
|
||||
"interface_enabled": "true",
|
||||
"frequency": int(sub_interface["frequency"]),
|
||||
"bandwidth": int(sub_interface["bandwidth"]),
|
||||
"txpower": int(sub_interface["txpower"]),
|
||||
"spreadingfactor": int(sub_interface["spreadingfactor"]),
|
||||
"codingrate": int(sub_interface["codingrate"]),
|
||||
"vport": int(sub_interface["vport"]),
|
||||
}
|
||||
|
||||
interfaces[interface_name] = interface_details
|
||||
|
||||
# handle SerialInterface, KISSInterface, and AX25KISSInterface
|
||||
if interface_type == "SerialInterface" or interface_type == "KISSInterface" or interface_type == "AX25KISSInterface":
|
||||
|
||||
# ensure port provided
|
||||
interface_port = data.get('port')
|
||||
if interface_port is None or interface_port == "":
|
||||
return web.json_response({
|
||||
"message": "Port is required",
|
||||
}, status=422)
|
||||
|
||||
# set required options
|
||||
interface_details["port"] = interface_port
|
||||
|
||||
# set optional options
|
||||
InterfaceEditor.update_value(interface_details, data, "speed")
|
||||
InterfaceEditor.update_value(interface_details, data, "databits")
|
||||
InterfaceEditor.update_value(interface_details, data, "parity")
|
||||
InterfaceEditor.update_value(interface_details, data, "stopbits")
|
||||
|
||||
# Handle KISS and AX25KISS specific options
|
||||
if interface_type == "KISSInterface" or interface_type == "AX25KISSInterface":
|
||||
|
||||
# set optional options
|
||||
InterfaceEditor.update_value(interface_details, data, "preamble")
|
||||
InterfaceEditor.update_value(interface_details, data, "txtail")
|
||||
InterfaceEditor.update_value(interface_details, data, "persistence")
|
||||
InterfaceEditor.update_value(interface_details, data, "slottime")
|
||||
InterfaceEditor.update_value(interface_details, data, "callsign")
|
||||
InterfaceEditor.update_value(interface_details, data, "ssid")
|
||||
|
||||
# FIXME: move to own sections
|
||||
# RNode Airtime limits and station ID
|
||||
InterfaceEditor.update_value(interface_details, data, "callsign")
|
||||
InterfaceEditor.update_value(interface_details, data, "id_interval")
|
||||
InterfaceEditor.update_value(interface_details, data, "airtime_limit_long")
|
||||
InterfaceEditor.update_value(interface_details, data, "airtime_limit_short")
|
||||
|
||||
# handle Pipe Interface
|
||||
if interface_type == "PipeInterface":
|
||||
|
||||
# ensure command provided
|
||||
interface_command = data.get('command')
|
||||
if interface_command is None or interface_command == "":
|
||||
return web.json_response({
|
||||
"message": "Command is required",
|
||||
}, status=422)
|
||||
|
||||
# ensure command provided
|
||||
interface_respawn_delay = data.get('respawn_delay')
|
||||
if interface_respawn_delay is None or interface_respawn_delay == "":
|
||||
return web.json_response({
|
||||
"message": "Respawn delay is required",
|
||||
}, status=422)
|
||||
|
||||
# set required options
|
||||
interface_details["command"] = interface_command
|
||||
interface_details["respawn_delay"] = interface_respawn_delay
|
||||
|
||||
# set common interface options
|
||||
InterfaceEditor.update_value(interface_details, data, "bitrate")
|
||||
InterfaceEditor.update_value(interface_details, data, "mode")
|
||||
InterfaceEditor.update_value(interface_details, data, "network_name")
|
||||
InterfaceEditor.update_value(interface_details, data, "passphrase")
|
||||
InterfaceEditor.update_value(interface_details, data, "ifac_size")
|
||||
|
||||
# merge new interface into existing interfaces
|
||||
interfaces[interface_name] = interface_details
|
||||
@@ -617,9 +795,22 @@ class ReticulumMeshChat:
|
||||
# add interface to output
|
||||
output.append(f"[[{interface_name}]]")
|
||||
for key, value in interface.items():
|
||||
if not isinstance(value, dict):
|
||||
output.append(f" {key} = {value}")
|
||||
output.append("")
|
||||
|
||||
# Handle sub-interfaces for RNodeMultiInterface
|
||||
if interface.get("type") == "RNodeMultiInterface":
|
||||
for sub_name, sub_config in interface.items():
|
||||
if sub_name in {"type", "port", "interface_enabled"}:
|
||||
continue
|
||||
if isinstance(sub_config, dict):
|
||||
output.append(f" [[[{sub_name}]]]")
|
||||
for sub_key, sub_value in sub_config.items():
|
||||
output.append(f" {sub_key} = {sub_value}")
|
||||
output.append("")
|
||||
|
||||
|
||||
return web.Response(
|
||||
text="\n".join(output),
|
||||
content_type="text/plain",
|
||||
|
||||
@@ -1,50 +1,31 @@
|
||||
import RNS.vendor.configobj
|
||||
|
||||
|
||||
class InterfaceConfigParser:
|
||||
|
||||
@staticmethod
|
||||
def parse(text):
|
||||
|
||||
# process config
|
||||
# get lines from provided text
|
||||
lines = text.splitlines()
|
||||
|
||||
# ensure [interfaces] section exists
|
||||
if "[interfaces]" not in lines:
|
||||
lines.insert(0, "[interfaces]")
|
||||
|
||||
# parse lines as rns config object
|
||||
config = RNS.vendor.configobj.ConfigObj(lines)
|
||||
|
||||
# get interfaces from config
|
||||
config_interfaces = config.get("interfaces")
|
||||
|
||||
# process interfaces
|
||||
interfaces = []
|
||||
current_interface = None
|
||||
for line in text.splitlines():
|
||||
for interface_name in config_interfaces:
|
||||
|
||||
# strip whitespace from either side of string
|
||||
line = line.strip()
|
||||
|
||||
# skip empty lines and commented out lines
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
# check if we found a line containing an interface name
|
||||
if line.startswith("[[") and line.endswith("]]"):
|
||||
|
||||
# we found a new interface, so add the previously parsed one to the interfaces list
|
||||
if current_interface:
|
||||
interfaces.append(current_interface)
|
||||
|
||||
# get interface name by removing the "[[" and "]]"
|
||||
interface_name = line[2:-2]
|
||||
current_interface = {
|
||||
"name": interface_name,
|
||||
}
|
||||
|
||||
# we found a key=value line, so we will set these values on the interface
|
||||
elif current_interface is not None and "=" in line:
|
||||
|
||||
# parse key and value
|
||||
line_parts = line.split("=", 1)
|
||||
key = line_parts[0].strip()
|
||||
value = line_parts[1].strip().strip('"').strip("'")
|
||||
|
||||
# skip empty values or values equal to "None"
|
||||
if value is None or value == "None":
|
||||
continue
|
||||
|
||||
# set key/value on interface
|
||||
current_interface[key] = value
|
||||
|
||||
# add the final interface to the list
|
||||
if current_interface:
|
||||
interfaces.append(current_interface)
|
||||
# ensure interface has a name
|
||||
interface_config = config_interfaces[interface_name]
|
||||
interface_config["name"] = interface_name
|
||||
interfaces.append(interface_config)
|
||||
|
||||
return interfaces
|
||||
|
||||
14
src/backend/interface_editor.py
Normal file
14
src/backend/interface_editor.py
Normal file
@@ -0,0 +1,14 @@
|
||||
class InterfaceEditor:
|
||||
|
||||
@staticmethod
|
||||
def update_value(interface_details: dict, data: dict, key: str):
|
||||
|
||||
# update value if provided and not empty
|
||||
value = data.get(key)
|
||||
if value is not None and value != "":
|
||||
interface_details[key] = value
|
||||
return
|
||||
|
||||
# otherwise remove existing value
|
||||
if key in interface_details:
|
||||
del interface_details[key]
|
||||
10
src/frontend/components/forms/FormLabel.vue
Normal file
10
src/frontend/components/forms/FormLabel.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<label class="block text-sm font-medium text-gray-900 dark:text-zinc-100">
|
||||
<slot/>
|
||||
</label>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormLabel',
|
||||
}
|
||||
</script>
|
||||
10
src/frontend/components/forms/FormSubLabel.vue
Normal file
10
src/frontend/components/forms/FormSubLabel.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="text-xs text-gray-600 dark:text-zinc-300">
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormSubLabel',
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
35
src/frontend/components/interfaces/ExpandingSection.vue
Normal file
35
src/frontend/components/interfaces/ExpandingSection.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="bg-white rounded shadow divide-y divide-gray-300 dark:divide-zinc-700 dark:bg-zinc-900 overflow-hidden">
|
||||
<div @click="isExpanded = !isExpanded" class="flex p-2 justify-between cursor-pointer hover:bg-gray-50 dark:hover:bg-zinc-800">
|
||||
<div class="my-auto mr-auto">
|
||||
<div class="font-bold dark:text-white">
|
||||
<slot name="title"/>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
<slot name="subtitle"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-auto ml-2">
|
||||
<div class="w-5 h-5 text-gray-600 dark:text-gray-300 transform transition-transform duration-200" :class="{ 'rotate-90': isExpanded }">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="size-5">
|
||||
<rect width="256" height="256" fill="none"/>
|
||||
<path d="M181.66,122.34l-80-80A8,8,0,0,0,88,48V208a8,8,0,0,0,13.66,5.66l80-80A8,8,0,0,0,181.66,122.34Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isExpanded" class="divide-y divide-gray-200 dark:text-white">
|
||||
<slot name="content"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExpandingSection',
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -36,6 +36,14 @@
|
||||
<path d="M252.44,121.34l-48-32A8,8,0,0,0,192,96v24H72V72h33a32,32,0,1,0,0-16H72A16,16,0,0,0,56,72v48H8a8,8,0,0,0,0,16H56v48a16,16,0,0,0,16,16h32v8a16,16,0,0,0,16,16h32a16,16,0,0,0,16-16V176a16,16,0,0,0-16-16H120a16,16,0,0,0-16,16v8H72V136H192v24a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM136,48a16,16,0,1,1-16,16A16,16,0,0,1,136,48ZM120,176h32v32H120Zm88-30.95V111l25.58,17Z"></path>
|
||||
</svg>
|
||||
|
||||
<svg v-else-if="iface.type === 'RNodeMultiInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"></path></svg>
|
||||
|
||||
<svg v-else-if="iface.type === 'I2PInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M72,92A12,12,0,1,1,60,80,12,12,0,0,1,72,92Zm56-12a12,12,0,1,0,12,12A12,12,0,0,0,128,80Zm68,24a12,12,0,1,0-12-12A12,12,0,0,0,196,104ZM60,152a12,12,0,1,0,12,12A12,12,0,0,0,60,152Zm68,0a12,12,0,1,0,12,12A12,12,0,0,0,128,152Zm68,0a12,12,0,1,0,12,12A12,12,0,0,0,196,152Z"></path></svg>
|
||||
|
||||
<svg v-else-if="iface.type === 'KISSInterface' || iface.type === 'AX25KISSInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M104,168a8,8,0,0,1-8,8H64a8,8,0,0,1,0-16H96A8,8,0,0,1,104,168Zm-8-40H64a8,8,0,0,0,0,16H96a8,8,0,0,0,0-16Zm0-32H64a8,8,0,0,0,0,16H96a8,8,0,0,0,0-16ZM232,80V192a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V72a8,8,0,0,1,5.7-7.66l160-48a8,8,0,0,1,4.6,15.33L86.51,64H216A16,16,0,0,1,232,80ZM216,192V80H40V192H216Zm-16-56a40,40,0,1,1-40-40A40,40,0,0,1,200,136Zm-16,0a24,24,0,1,0-24,24A24,24,0,0,0,184,136Z"></path></svg>
|
||||
|
||||
<svg v-else-if="iface.type === 'PipeInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"></path></svg>
|
||||
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white">
|
||||
<path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path>
|
||||
</svg>
|
||||
@@ -186,6 +194,10 @@
|
||||
<div>• Bitrate: {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</div>
|
||||
<div>• TX: {{ formatBytes(iface._stats?.txb ?? 0) }}</div>
|
||||
<div>• RX: {{ formatBytes(iface._stats?.rxb ?? 0) }}</div>
|
||||
<div v-if="iface.type === 'RNodeInterface'">• Noise Floor: {{
|
||||
iface._stats?.noise_floor
|
||||
}} dBm
|
||||
</div>
|
||||
<div v-if="iface._stats?.clients">• Clients: {{ iface._stats?.clients }}</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
<div class="my-auto">Loading {{ nodePageProgress }}%</div>
|
||||
</div>
|
||||
<pre v-else v-html="nodePageContent" class="h-full text-wrap"></pre>
|
||||
<pre v-else v-html="nodePageContent" class="h-full break-words whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
|
||||
<!-- file download bottom bar -->
|
||||
|
||||
@@ -143,7 +143,7 @@ class Utils {
|
||||
|
||||
static isInterfaceEnabled(iface) {
|
||||
const rawValue = iface.enabled ?? iface.interface_enabled;
|
||||
const value = rawValue?.toLowerCase();
|
||||
const value = rawValue?.toString()?.toLowerCase();
|
||||
return value === "on" || value === "yes" || value === "true";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user