Add public key to identity details and enhance companion configuration handling

- Updated IdentityManager to include public key in identity details when retrieving identities.
- Introduced a new method in RepeaterDaemon for adding companions from configuration, supporting hot-reload functionality.
- Enhanced error handling for companion registration, ensuring proper validation of identity keys and settings.
- Updated API endpoints to include configured companions in the response, improving visibility of companion status and configuration.
This commit is contained in:
agessaman
2026-03-05 16:26:50 -08:00
parent b6757a0ca0
commit a6170f70ed
37 changed files with 464 additions and 140 deletions
+1
View File
@@ -51,6 +51,7 @@ class IdentityManager:
"name": name,
"type": id_type,
"address": identity.get_address_bytes().hex() if identity else "N/A",
"public_key": identity.get_public_key().hex() if identity else None,
}
)
return identities
+147
View File
@@ -511,6 +511,153 @@ class RepeaterDaemon:
except Exception as e:
logger.error(f"Failed to load companion '{name}': {e}", exc_info=True)
async def add_companion_from_config(self, comp_config: dict) -> None:
"""
Load a single companion from config and register it (hot-reload).
Creates RepeaterCompanionBridge, CompanionFrameServer, starts the server,
and registers with identity_manager. Raises on error.
"""
from pymc_core import LocalIdentity
from pymc_core.companion.models import Channel
from repeater.companion import CompanionFrameServer, RepeaterCompanionBridge
from repeater.companion.constants import DEFAULT_PUBLIC_CHANNEL_SECRET
name = comp_config.get("name")
identity_key = comp_config.get("identity_key")
settings = comp_config.get("settings") or {}
if not name or not identity_key:
raise ValueError("Companion config missing name or identity_key")
if isinstance(identity_key, str):
try:
identity_key_bytes = bytes.fromhex(identity_key)
except ValueError as e:
raise ValueError(f"Companion '{name}' identity_key invalid hex: {e}") from e
elif isinstance(identity_key, bytes):
identity_key_bytes = identity_key
else:
raise ValueError(f"Companion '{name}' identity_key has unknown type")
if len(identity_key_bytes) not in (32, 64):
raise ValueError(
f"Companion '{name}' identity_key must be 32 bytes (hex) or 64 bytes (MeshCore firmware key)"
)
# Already registered?
if name in self.identity_manager.named_identities:
raise ValueError(f"Companion '{name}' is already registered")
identity = LocalIdentity(seed=identity_key_bytes)
pubkey = identity.get_public_key()
companion_hash = pubkey[0]
companion_hash_str = f"0x{companion_hash:02x}"
if companion_hash in self.companion_bridges:
raise ValueError(f"Companion with hash 0x{companion_hash:02x} already loaded")
sqlite_handler = None
if self.repeater_handler and self.repeater_handler.storage:
sqlite_handler = self.repeater_handler.storage.sqlite_handler
radio_config = (
self.repeater_handler.radio_config
if self.repeater_handler
else self.config.get("radio", {})
)
node_name = settings.get("node_name", name)
tcp_port = settings.get("tcp_port", 5000)
bind_address = settings.get("bind_address", "0.0.0.0")
bridge = RepeaterCompanionBridge(
identity=identity,
packet_injector=self.router.inject_packet,
node_name=node_name,
radio_config=radio_config,
sqlite_handler=sqlite_handler,
companion_hash=companion_hash_str,
)
if sqlite_handler:
contact_rows = sqlite_handler.companion_load_contacts(companion_hash_str)
if contact_rows:
records = []
for row in contact_rows:
d = dict(row)
d["public_key"] = d.pop("pubkey", d.get("public_key", b""))
records.append(d)
bridge.contacts.load_from_dicts(records)
channel_rows = sqlite_handler.companion_load_channels(companion_hash_str)
for row in channel_rows:
s = row.get("secret", b"")
if isinstance(s, bytes):
raw = s
elif isinstance(s, (bytearray, memoryview)):
raw = bytes(s)
elif s:
raw = bytes.fromhex(s if isinstance(s, str) else str(s))
else:
raw = b""
if len(raw) < 32:
raw = raw + b"\x00" * (32 - len(raw))
elif len(raw) > 32:
raw = raw[:32]
ch = Channel(name=row.get("name", ""), secret=raw)
bridge.channels.set(row.get("channel_idx", 0), ch)
for msg_dict in sqlite_handler.companion_load_messages(companion_hash_str):
from pymc_core.companion.models import QueuedMessage
sk = msg_dict.get("sender_key", b"")
if isinstance(sk, str):
sk = bytes.fromhex(sk)
bridge.message_queue.push(
QueuedMessage(
sender_key=sk,
txt_type=msg_dict.get("txt_type", 0),
timestamp=msg_dict.get("timestamp", 0),
text=msg_dict.get("text", ""),
is_channel=bool(msg_dict.get("is_channel", False)),
channel_idx=msg_dict.get("channel_idx", 0),
path_len=msg_dict.get("path_len", 0),
)
)
if bridge.get_channel(0) is None:
bridge.set_channel(0, "Public", DEFAULT_PUBLIC_CHANNEL_SECRET)
self.companion_bridges[companion_hash] = bridge
frame_server = CompanionFrameServer(
bridge=bridge,
companion_hash=companion_hash_str,
port=tcp_port,
bind_address=bind_address,
sqlite_handler=sqlite_handler,
local_hash=self.local_hash,
stats_getter=self._get_companion_stats,
control_handler=(
self.discovery_helper.control_handler if self.discovery_helper else None
),
)
await frame_server.start()
self.companion_frame_servers.append(frame_server)
self.identity_manager.register_identity(
name=name,
identity=identity,
config=comp_config,
identity_type="companion",
)
logger.info(
f"Hot-reload: Loaded companion '{name}': hash=0x{companion_hash:02x}, "
f"port={tcp_port}, bind={bind_address}"
)
async def _on_raw_rx_for_companions(self, data: bytes, rssi: int, snr: float) -> None:
"""Raw RX subscriber: push PUSH_CODE_LOG_RX_DATA (0x88) to connected companion clients."""
servers = getattr(self, "companion_frame_servers", [])
+228 -54
View File
@@ -1234,9 +1234,9 @@ class APIEndpoints:
self.config["radio"]["cad"]["min_threshold"] = min_val
config_path = getattr(self, "_config_path", "/etc/pymc_repeater/config.yaml")
saved, err = self.config_manager.save_to_file()
saved = self.config_manager.save_to_file()
if not saved:
return self._error(err or "Failed to save configuration to file")
return self._error("Failed to save configuration to file")
logger.info(
f"Saved CAD settings to config: peak={peak}, min={min_val}, rate={detection_rate:.1f}%"
@@ -1766,14 +1766,14 @@ class APIEndpoints:
# Update the configuration file using ConfigManager
try:
saved, err = self.config_manager.save_to_file()
saved = self.config_manager.save_to_file()
if saved:
logger.info(
f"Updated running config and saved global flood policy to file: {'allow' if global_flood_allow else 'deny'}"
)
else:
logger.error(f"Failed to save global flood policy to file: {err}")
return self._error(err or "Failed to save configuration to file")
logger.error("Failed to save global flood policy to file")
return self._error("Failed to save configuration to file")
except Exception as e:
logger.error(f"Failed to save global flood policy to file: {e}")
return self._error(f"Failed to save configuration to file: {e}")
@@ -1961,7 +1961,7 @@ class APIEndpoints:
identities_config = self.config.get("identities", {})
room_servers = identities_config.get("room_servers") or []
# Enhance with config data
# Enhance with config data (room servers)
configured = []
for room_config in room_servers:
name = room_config.get("name")
@@ -1988,12 +1988,46 @@ class APIEndpoints:
}
)
# Configured companions (same pattern as room servers)
companions = identities_config.get("companions") or []
configured_companions = []
for comp_config in companions:
name = comp_config.get("name")
identity_key = comp_config.get("identity_key", "")
settings = comp_config.get("settings", {})
matching = next(
(
r
for r in registered_identities
if r["name"] == f"companion:{name}"
),
None,
)
configured_companions.append(
{
"name": name,
"type": "companion",
"identity_key": (
identity_key[:16] + "..." if len(identity_key) > 16 else identity_key
),
"identity_key_length": len(identity_key),
"settings": settings,
"hash": matching["hash"] if matching else None,
"public_key": matching.get("public_key") if matching else None,
"registered": matching is not None,
}
)
return self._success(
{
"registered": registered_identities,
"configured": configured,
"configured_companions": configured_companions,
"total_registered": len(registered_identities),
"total_configured": len(configured),
"total_configured_companions": len(configured_companions),
}
)
@@ -2019,14 +2053,17 @@ class APIEndpoints:
identities_config = self.config.get("identities", {})
room_servers = identities_config.get("room_servers") or []
companions = identities_config.get("companions") or []
# Find the identity in config
# Find the identity in config (room servers first, then companions)
identity_config = next((r for r in room_servers if r.get("name") == name), None)
if identity_config is None:
identity_config = next((c for c in companions if c.get("name") == name), None)
if not identity_config:
return self._error(f"Identity '{name}' not found")
# Get runtime info if available
# Get runtime info if available (identity_manager uses name for both types)
if self.daemon_instance and hasattr(self.daemon_instance, "identity_manager"):
identity_manager = self.daemon_instance.identity_manager
runtime_info = identity_manager.get_identity_by_name(name)
@@ -2087,11 +2124,18 @@ class APIEndpoints:
if not name:
return self._error("Missing required field: name")
# Validate passwords are different if both provided
admin_pw = settings.get("admin_password")
guest_pw = settings.get("guest_password")
if admin_pw and guest_pw and admin_pw == guest_pw:
return self._error("admin_password and guest_password must be different")
# Validate identity type
if identity_type not in ["room_server", "companion"]:
return self._error(
f"Invalid identity type: {identity_type}. Only 'room_server' and 'companion' are supported."
)
# Room server: validate passwords are different if both provided
if identity_type == "room_server":
admin_pw = settings.get("admin_password")
guest_pw = settings.get("guest_password")
if admin_pw and guest_pw and admin_pw == guest_pw:
return self._error("admin_password and guest_password must be different")
# Auto-generate identity key if not provided
key_was_generated = False
@@ -2106,38 +2150,58 @@ class APIEndpoints:
logger.error(f"Failed to auto-generate identity key: {gen_error}")
return self._error(f"Failed to auto-generate identity key: {gen_error}")
# Validate identity type
if identity_type not in ["room_server"]:
return self._error(
f"Invalid identity type: {identity_type}. Only 'room_server' is supported."
)
# Check if identity already exists
identities_config = self.config.get("identities", {})
room_servers = identities_config.get("room_servers") or []
if any(r.get("name") == name for r in room_servers):
return self._error(f"Identity with name '{name}' already exists")
# Create new identity config
new_identity = {
"name": name,
"identity_key": identity_key,
"type": identity_type,
"settings": settings,
}
# Add to config
room_servers.append(new_identity)
if "identities" not in self.config:
self.config["identities"] = {}
self.config["identities"]["room_servers"] = room_servers
if identity_type == "companion":
# Companion: validate key length (32 or 64 bytes hex), normalize settings
if identity_key:
try:
key_bytes = bytes.fromhex(identity_key)
if len(key_bytes) not in (32, 64):
return self._error(
"Companion identity_key must be 32 or 64 bytes (64 or 128 hex chars)"
)
except ValueError:
return self._error("Companion identity_key must be a valid hex string")
companions = identities_config.get("companions") or []
if any(c.get("name") == name for c in companions):
return self._error(f"Companion with name '{name}' already exists")
comp_settings = {
"node_name": settings.get("node_name") or name,
"tcp_port": settings.get("tcp_port", 5000),
"bind_address": settings.get("bind_address", "0.0.0.0"),
}
new_identity = {
"name": name,
"identity_key": identity_key,
"type": identity_type,
"settings": comp_settings,
}
companions.append(new_identity)
self.config["identities"]["companions"] = companions
else:
# Room server
room_servers = identities_config.get("room_servers") or []
if any(r.get("name") == name for r in room_servers):
return self._error(f"Identity with name '{name}' already exists")
new_identity = {
"name": name,
"identity_key": identity_key,
"type": identity_type,
"settings": settings,
}
room_servers.append(new_identity)
self.config["identities"]["room_servers"] = room_servers
# Save to file
saved, err = self.config_manager.save_to_file()
saved = self.config_manager.save_to_file()
if not saved:
return self._error(err or "Failed to save configuration to file")
return self._error("Failed to save configuration to file")
logger.info(
f"Created new identity: {name} (type: {identity_type}){' with auto-generated key' if key_was_generated else ''}"
@@ -2145,7 +2209,7 @@ class APIEndpoints:
# Hot reload - register identity immediately
registration_success = False
if self.daemon_instance:
if identity_type == "room_server" and self.daemon_instance:
try:
from pymc_core import LocalIdentity
@@ -2188,11 +2252,35 @@ class APIEndpoints:
f"Failed to hot reload identity {name}: {reg_error}", exc_info=True
)
message = (
f"Identity '{name}' created successfully and activated immediately!"
if registration_success
else f"Identity '{name}' created successfully. Restart required to activate."
)
elif identity_type == "companion" and self.daemon_instance and self.event_loop:
try:
import asyncio
future = asyncio.run_coroutine_threadsafe(
self.daemon_instance.add_companion_from_config(new_identity),
self.event_loop,
)
future.result(timeout=15)
registration_success = True
logger.info(f"Hot reload: Companion '{name}' activated immediately")
except Exception as comp_error:
logger.warning(
f"Hot reload companion '{name}' failed: {comp_error}. Restart required to activate.",
exc_info=True,
)
if identity_type == "companion":
message = (
f"Companion '{name}' created successfully and activated immediately!"
if registration_success
else f"Companion '{name}' created successfully. Restart required to activate."
)
else:
message = (
f"Identity '{name}' created successfully and activated immediately!"
if registration_success
else f"Identity '{name}' created successfully. Restart required to activate."
)
if key_was_generated:
message += " Identity key was auto-generated."
@@ -2242,10 +2330,63 @@ class APIEndpoints:
if not name:
return self._error("Missing required field: name")
identities_config = self.config.get("identities", {})
room_servers = identities_config.get("room_servers") or []
identity_type = data.get("type", "room_server")
if identity_type not in ["room_server", "companion"]:
return self._error(
f"Invalid identity type: {identity_type}. Only 'room_server' and 'companion' are supported."
)
# Find the identity
identities_config = self.config.get("identities", {})
if identity_type == "companion":
companions = identities_config.get("companions") or []
identity_index = next(
(i for i, c in enumerate(companions) if c.get("name") == name), None
)
if identity_index is None:
return self._error(f"Companion '{name}' not found")
identity = companions[identity_index]
if "new_name" in data:
new_name = data["new_name"]
if any(
c.get("name") == new_name
for i, c in enumerate(companions)
if i != identity_index
):
return self._error(f"Companion with name '{new_name}' already exists")
identity["name"] = new_name
if "identity_key" in data and data["identity_key"]:
new_key = data["identity_key"]
if "..." not in new_key:
try:
key_bytes = bytes.fromhex(new_key)
if len(key_bytes) in (32, 64):
identity["identity_key"] = new_key
logger.info(f"Updated identity_key for companion '{name}'")
except ValueError:
pass
if "settings" in data:
if "settings" not in identity:
identity["settings"] = {}
# Only allow companion settings
for k, v in data["settings"].items():
if k in ("node_name", "tcp_port", "bind_address"):
identity["settings"][k] = v
companions[identity_index] = identity
self.config["identities"]["companions"] = companions
saved = self.config_manager.save_to_file()
if not saved:
return self._error("Failed to save configuration to file")
logger.info(f"Updated companion: {name}")
message = f"Companion '{name}' updated successfully. Restart required to apply changes."
return self._success(identity, message=message)
# Room server path
room_servers = identities_config.get("room_servers") or []
identity_index = next(
(i for i, r in enumerate(room_servers) if r.get("name") == name), None
)
@@ -2298,9 +2439,9 @@ class APIEndpoints:
room_servers[identity_index] = identity
self.config["identities"]["room_servers"] = room_servers
saved, err = self.config_manager.save_to_file()
saved = self.config_manager.save_to_file()
if not saved:
return self._error(err or "Failed to save configuration to file")
return self._error("Failed to save configuration to file")
logger.info(f"Updated identity: {name}")
@@ -2380,9 +2521,9 @@ class APIEndpoints:
@cherrypy.expose
@cherrypy.tools.json_out()
def delete_identity(self, name=None):
def delete_identity(self, name=None, type=None):
"""
DELETE /api/delete_identity?name=<name> - Delete an identity
DELETE /api/delete_identity?name=<name>&type=<room_server|companion> - Delete an identity
"""
# Enable CORS for this endpoint only if configured
self._set_cors_headers()
@@ -2399,7 +2540,40 @@ class APIEndpoints:
if not name:
return self._error("Missing name parameter")
identity_type = (type or "room_server").lower()
if identity_type not in ["room_server", "companion"]:
return self._error(
f"Invalid type: {type}. Use 'room_server' or 'companion'."
)
identities_config = self.config.get("identities", {})
if identity_type == "companion":
companions = identities_config.get("companions") or []
initial_count = len(companions)
companions = [c for c in companions if c.get("name") != name]
if len(companions) == initial_count:
return self._error(f"Companion '{name}' not found")
self.config["identities"]["companions"] = companions
saved = self.config_manager.save_to_file()
if not saved:
return self._error("Failed to save configuration to file")
logger.info(f"Deleted companion: {name}")
unregister_success = False
if self.daemon_instance and hasattr(self.daemon_instance, "identity_manager"):
identity_manager = self.daemon_instance.identity_manager
if name in identity_manager.named_identities:
del identity_manager.named_identities[name]
logger.info(f"Removed companion {name} from named_identities")
unregister_success = True
message = (
f"Companion '{name}' deleted successfully and deactivated immediately!"
if unregister_success
else f"Companion '{name}' deleted successfully. Restart required to fully remove."
)
return self._success({"name": name}, message=message)
# Room server path
room_servers = identities_config.get("room_servers") or []
# Find and remove the identity
@@ -2412,9 +2586,9 @@ class APIEndpoints:
# Update config
self.config["identities"]["room_servers"] = room_servers
saved, err = self.config_manager.save_to_file()
saved = self.config_manager.save_to_file()
if not saved:
return self._error(err or "Failed to save configuration to file")
return self._error("Failed to save configuration to file")
logger.info(f"Deleted identity: {name}")
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{a as p,b as n,g as m,e as t,s as g,t as s,j as d,p as l}from"./index-sHch0610.js";const f={class:"flex items-center justify-between mb-4"},w={class:"text-xl font-semibold text-content-primary dark:text-content-primary"},v={class:"mb-6"},h={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},y={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},C={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},B={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},j={class:"flex gap-3"},_=p({__name:"ConfirmDialog",props:{show:{type:Boolean},title:{default:"Confirm Action"},message:{},confirmText:{default:"Confirm"},cancelText:{default:"Cancel"},variant:{default:"warning"}},emits:["close","confirm"],setup(c,{emit:b}){const o=c,r=b,u=i=>{i.target===i.currentTarget&&r("close")},k={danger:"bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400",warning:"bg-yellow-100 dark:bg-yellow-500/20 border-yellow-500/30 text-yellow-600 dark:text-yellow-400",info:"bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400"},x={danger:"bg-red-500 hover:bg-red-600",warning:"bg-yellow-500 hover:bg-yellow-600",info:"bg-blue-500 hover:bg-blue-600"};return(i,e)=>o.show?(l(),n("div",{key:0,onClick:u,class:"fixed inset-0 bg-black/40 backdrop-blur-lg z-[99999] flex items-center justify-center p-4",style:{"backdrop-filter":"blur(8px) saturate(180%)",position:"fixed",top:"0",left:"0",right:"0",bottom:"0"}},[t("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl rounded-[20px] p-6 w-full max-w-md border border-stroke-subtle dark:border-white/10",onClick:e[3]||(e[3]=g(()=>{},["stop"]))},[t("div",f,[t("h3",w,s(o.title),1),t("button",{onClick:e[0]||(e[0]=a=>r("close")),class:"text-content-secondary dark:text-content-muted hover:text-content-primary dark:hover:text-content-primary transition-colors"},e[4]||(e[4]=[t("svg",{class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)]))]),t("div",v,[t("div",{class:d(["inline-flex p-3 rounded-xl mb-4",k[o.variant]])},[o.variant==="danger"?(l(),n("svg",h,e[5]||(e[5]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):o.variant==="warning"?(l(),n("svg",y,e[6]||(e[6]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):(l(),n("svg",C,e[7]||(e[7]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"},null,-1)])))],2),t("p",B,s(o.message),1)]),t("div",j,[t("button",{onClick:e[1]||(e[1]=a=>r("close")),class:"flex-1 px-4 py-3 rounded-xl bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary transition-all duration-200 border border-stroke-subtle dark:border-stroke/10"},s(o.cancelText),1),t("button",{onClick:e[2]||(e[2]=a=>r("confirm")),class:d(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",x[o.variant]])},s(o.confirmText),3)])])])):m("",!0)}});export{_};
import{a as p,b as n,g as m,e as t,s as g,t as s,j as d,p as l}from"./index-DyUIpN7m.js";const f={class:"flex items-center justify-between mb-4"},w={class:"text-xl font-semibold text-content-primary dark:text-content-primary"},v={class:"mb-6"},h={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},y={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},C={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},B={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},j={class:"flex gap-3"},_=p({__name:"ConfirmDialog",props:{show:{type:Boolean},title:{default:"Confirm Action"},message:{},confirmText:{default:"Confirm"},cancelText:{default:"Cancel"},variant:{default:"warning"}},emits:["close","confirm"],setup(c,{emit:b}){const o=c,r=b,u=i=>{i.target===i.currentTarget&&r("close")},k={danger:"bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400",warning:"bg-yellow-100 dark:bg-yellow-500/20 border-yellow-500/30 text-yellow-600 dark:text-yellow-400",info:"bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400"},x={danger:"bg-red-500 hover:bg-red-600",warning:"bg-yellow-500 hover:bg-yellow-600",info:"bg-blue-500 hover:bg-blue-600"};return(i,e)=>o.show?(l(),n("div",{key:0,onClick:u,class:"fixed inset-0 bg-black/40 backdrop-blur-lg z-[99999] flex items-center justify-center p-4",style:{"backdrop-filter":"blur(8px) saturate(180%)",position:"fixed",top:"0",left:"0",right:"0",bottom:"0"}},[t("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl rounded-[20px] p-6 w-full max-w-md border border-stroke-subtle dark:border-white/10",onClick:e[3]||(e[3]=g(()=>{},["stop"]))},[t("div",f,[t("h3",w,s(o.title),1),t("button",{onClick:e[0]||(e[0]=a=>r("close")),class:"text-content-secondary dark:text-content-muted hover:text-content-primary dark:hover:text-content-primary transition-colors"},e[4]||(e[4]=[t("svg",{class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)]))]),t("div",v,[t("div",{class:d(["inline-flex p-3 rounded-xl mb-4",k[o.variant]])},[o.variant==="danger"?(l(),n("svg",h,e[5]||(e[5]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):o.variant==="warning"?(l(),n("svg",y,e[6]||(e[6]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):(l(),n("svg",C,e[7]||(e[7]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"},null,-1)])))],2),t("p",B,s(o.message),1)]),t("div",j,[t("button",{onClick:e[1]||(e[1]=a=>r("close")),class:"flex-1 px-4 py-3 rounded-xl bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary transition-all duration-200 border border-stroke-subtle dark:border-stroke/10"},s(o.cancelText),1),t("button",{onClick:e[2]||(e[2]=a=>r("confirm")),class:d(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",x[o.variant]])},s(o.confirmText),3)])])])):m("",!0)}});export{_};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{a as e,b as r,i as o,p as n}from"./index-sHch0610.js";const d=e({name:"HelpView",__name:"Help",setup(a){return(i,t)=>(n(),r("div",null,t[0]||(t[0]=[o('<div class="glass-card backdrop-blur border border-stroke-subtle dark:border-white/10 rounded-[15px] p-8"><h1 class="text-content-primary dark:text-content-primary text-2xl font-semibold mb-6">Help &amp; Documentation</h1><div class="text-center py-12"><div class="text-primary mb-6"><svg class="w-20 h-20 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg></div><h2 class="text-content-primary dark:text-content-primary text-xl font-medium mb-3">pyMC Repeater Wiki</h2><p class="text-content-secondary dark:text-content-muted mb-8 max-w-md mx-auto"> Access documentation, setup guides, troubleshooting tips, and community resources on our official wiki. </p><a href="https://github.com/rightup/pyMC_Repeater/wiki" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 bg-primary hover:bg-primary/80 text-white dark:text-background font-medium py-3 px-6 rounded-xl transition-colors duration-200"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg> Visit Wiki Documentation </a><div class="mt-8 text-xs text-content-muted dark:text-content-muted"> Opens in a new tab </div></div></div>',1)])))}});export{d as default};
import{a as e,b as r,i as o,p as n}from"./index-DyUIpN7m.js";const d=e({name:"HelpView",__name:"Help",setup(a){return(i,t)=>(n(),r("div",null,t[0]||(t[0]=[o('<div class="glass-card backdrop-blur border border-stroke-subtle dark:border-white/10 rounded-[15px] p-8"><h1 class="text-content-primary dark:text-content-primary text-2xl font-semibold mb-6">Help &amp; Documentation</h1><div class="text-center py-12"><div class="text-primary mb-6"><svg class="w-20 h-20 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg></div><h2 class="text-content-primary dark:text-content-primary text-xl font-medium mb-3">pyMC Repeater Wiki</h2><p class="text-content-secondary dark:text-content-muted mb-8 max-w-md mx-auto"> Access documentation, setup guides, troubleshooting tips, and community resources on our official wiki. </p><a href="https://github.com/rightup/pyMC_Repeater/wiki" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 bg-primary hover:bg-primary/80 text-white dark:text-background font-medium py-3 px-6 rounded-xl transition-colors duration-200"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg> Visit Wiki Documentation </a><div class="mt-8 text-xs text-content-muted dark:text-content-muted"> Opens in a new tab </div></div></div>',1)])))}});export{d as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{a as k,b as o,g,e as r,j as a,t as p,s as x,p as s}from"./index-DyUIpN7m.js";const f={class:"mb-6"},m={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},v={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},h={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},w={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},C={class:"flex"},B=k({__name:"MessageDialog",props:{show:{type:Boolean},message:{},variant:{default:"success"}},emits:["close"],setup(i,{emit:d}){const t=i,l=d,c=n=>{n.target===n.currentTarget&&l("close")},b={success:"bg-green-100 dark:bg-green-500/20 border-green-600/40 dark:border-green-500/30 text-green-600 dark:text-green-400",error:"bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400",info:"bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400"},u={success:"bg-green-500 hover:bg-green-600",error:"bg-red-500 hover:bg-red-600",info:"bg-blue-500 hover:bg-blue-600"};return(n,e)=>t.show?(s(),o("div",{key:0,onClick:c,class:"fixed inset-0 bg-black/40 backdrop-blur-lg z-[99999] flex items-center justify-center p-4",style:{"backdrop-filter":"blur(8px) saturate(180%)",position:"fixed",top:"0",left:"0",right:"0",bottom:"0"}},[r("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl rounded-[20px] p-6 w-full max-w-md border border-stroke-subtle dark:border-white/10",onClick:e[1]||(e[1]=x(()=>{},["stop"]))},[r("div",f,[r("div",{class:a(["inline-flex p-3 rounded-xl mb-4",b[t.variant]])},[t.variant==="success"?(s(),o("svg",m,e[2]||(e[2]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"},null,-1)]))):t.variant==="error"?(s(),o("svg",v,e[3]||(e[3]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"},null,-1)]))):(s(),o("svg",h,e[4]||(e[4]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"},null,-1)])))],2),r("p",w,p(t.message),1)]),r("div",C,[r("button",{onClick:e[0]||(e[0]=y=>l("close")),class:a(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",u[t.variant]])}," OK ",2)])])])):g("",!0)}});export{B as _};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.plotly-chart[data-v-8daccd7e]{background:transparent!important}
@@ -1 +0,0 @@
.plotly-chart[data-v-c51a7a30]{background:transparent!important}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{L as J,a as zl,r as ut,o as Hl,$ as Ul,P as Rn,D as ql,b as tt,e as Z,g as Yt,t as Is,w as Kl,v as Vl,X as Ji,j as Tn,s as jl,p as it,x as Gl}from"./index-sHch0610.js";/**
import{L as J,a as zl,r as ut,o as Hl,$ as Ul,P as Rn,D as ql,b as tt,e as Z,g as Yt,t as Is,w as Kl,v as Vl,X as Ji,j as Tn,s as jl,p as it,x as Gl}from"./index-DyUIpN7m.js";/**
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
* @license MIT
*
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22 -22
View File
@@ -233,7 +233,7 @@ float cookTorranceSpecular(
float G1 = (2.0 * NdotH * VdotN) / VdotH;
float G2 = (2.0 * NdotH * LdotN) / LdotH;
float G = min(1.0, min(G1, G2));
//Distribution term
float D = beckmannDistribution(NdotH, roughness);
@@ -245,7 +245,7 @@ float cookTorranceSpecular(
}
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -393,7 +393,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -511,7 +511,7 @@ float cookTorranceSpecular(
float G1 = (2.0 * NdotH * VdotN) / VdotH;
float G2 = (2.0 * NdotH * LdotN) / LdotH;
float G = min(1.0, min(G1, G2));
//Distribution term
float D = beckmannDistribution(NdotH, roughness);
@@ -525,7 +525,7 @@ float cookTorranceSpecular(
//#pragma glslify: beckmann = require(glsl-specular-beckmann) // used in gl-surface3d
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -604,7 +604,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -639,7 +639,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -713,7 +713,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -746,7 +746,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -861,7 +861,7 @@ float beckmannSpecular(
}
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -963,7 +963,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1011,7 +1011,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1068,7 +1068,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1126,7 +1126,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1186,7 +1186,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1222,7 +1222,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1526,7 +1526,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1673,7 +1673,7 @@ float cookTorranceSpecular(
float G1 = (2.0 * NdotH * VdotN) / VdotH;
float G2 = (2.0 * NdotH * LdotN) / LdotH;
float G = min(1.0, min(G1, G2));
//Distribution term
float D = beckmannDistribution(NdotH, roughness);
@@ -1685,7 +1685,7 @@ float cookTorranceSpecular(
}
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1796,7 +1796,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1871,7 +1871,7 @@ void main() {
#define GLSLIFY 1
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1957,7 +1957,7 @@ vec4 packFloat(float v) {
}
bool outOfRange(float a, float b, float p) {
return ((p > max(a, b)) ||
return ((p > max(a, b)) ||
(p < min(a, b)));
}
@@ -1 +1 @@
import{M as x,c as s}from"./index-sHch0610.js";const l={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},d=-116,i=8,u=5;function y(t,e){return t-e}function S(t){return l[t]??l[i]}function f(t,e){const r=e+u;if(t<=e){const o=t<=e-5?0:1;return{bars:o,color:"text-red-600 dark:text-red-400",snr:t,quality:o===0?"none":"poor"}}if(t<r){const n=(t-e)/u<.5?2:3;return{bars:n,color:n===2?"text-orange-600 dark:text-orange-400":"text-yellow-600 dark:text-yellow-400",snr:t,quality:"fair"}}const a=t-r>=10?5:4;return{bars:a,color:a===5?"text-green-600 dark:text-green-400":"text-green-600 dark:text-green-300",snr:t,quality:a===5?"excellent":"good"}}function N(){const t=x(),e=s(()=>t.noiseFloorDbm??d),r=s(()=>t.stats?.config?.radio?.spreading_factor??i),c=s(()=>S(r.value));return{getSignalQuality:o=>{if(!o||o>0||o<-120)return{bars:0,color:"text-gray-400 dark:text-gray-500",snr:-999,quality:"none"};const n=y(o,e.value),g=Math.max(-30,Math.min(20,n));return f(g,c.value)},noiseFloor:e,spreadingFactor:r,minSNR:c}}export{N as u};
import{M as x,c as s}from"./index-DyUIpN7m.js";const l={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},d=-116,i=8,u=5;function y(t,e){return t-e}function S(t){return l[t]??l[i]}function f(t,e){const r=e+u;if(t<=e){const o=t<=e-5?0:1;return{bars:o,color:"text-red-600 dark:text-red-400",snr:t,quality:o===0?"none":"poor"}}if(t<r){const n=(t-e)/u<.5?2:3;return{bars:n,color:n===2?"text-orange-600 dark:text-orange-400":"text-yellow-600 dark:text-yellow-400",snr:t,quality:"fair"}}const a=t-r>=10?5:4;return{bars:a,color:a===5?"text-green-600 dark:text-green-400":"text-green-600 dark:text-green-300",snr:t,quality:a===5?"excellent":"good"}}function N(){const t=x(),e=s(()=>t.noiseFloorDbm??d),r=s(()=>t.stats?.config?.radio?.spreading_factor??i),c=s(()=>S(r.value));return{getSignalQuality:o=>{if(!o||o>0||o<-120)return{bars:0,color:"text-gray-400 dark:text-gray-500",snr:-999,quality:"none"};const n=y(o,e.value),g=Math.max(-30,Math.min(20,n));return f(g,c.value)},noiseFloor:e,spreadingFactor:r,minSNR:c}}export{N as u};
+2 -2
View File
@@ -8,8 +8,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-sHch0610.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dk6Oh8NN.css">
<script type="module" crossorigin src="/assets/index-DyUIpN7m.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D-3p9FIW.css">
</head>
<body>
<div id="app"></div>