diff --git a/manage.sh b/manage.sh
index 64b5957..db90bac 100755
--- a/manage.sh
+++ b/manage.sh
@@ -236,6 +236,13 @@ install_repeater() {
echo " Generated version: $GENERATED_VERSION"
fi
+ echo "29"; echo "# Cleaning old installation files..."
+ # Remove old repeater directory to ensure clean install
+ rm -rf "$INSTALL_DIR/repeater" 2>/dev/null || true
+ # Clean up old Python bytecode
+ find "$INSTALL_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
+ find "$INSTALL_DIR" -type f -name '*.pyc' -delete 2>/dev/null || true
+
echo "30"; echo "# Installing files..."
cp -r "$SCRIPT_DIR/repeater" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/pyproject.toml" "$INSTALL_DIR/"
@@ -411,6 +418,14 @@ upgrade_repeater() {
fi
echo " ✓ Version file generated"
+ echo "[3.8/9] Cleaning old installation files..."
+ # Remove old repeater directory to ensure clean upgrade
+ rm -rf "$INSTALL_DIR/repeater" 2>/dev/null || true
+ # Clean up old Python bytecode
+ find "$INSTALL_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
+ find "$INSTALL_DIR" -type f -name '*.pyc' -delete 2>/dev/null || true
+ echo " ✓ Old files cleaned"
+
echo "[4/9] Installing new files..."
cp -r repeater "$INSTALL_DIR/" 2>/dev/null || true
cp pyproject.toml "$INSTALL_DIR/" 2>/dev/null || true
@@ -465,8 +480,18 @@ EOF
# Suppress pip root user warnings
export PIP_ROOT_USER_ACTION=ignore
- # First, upgrade the package and dependencies (only updates what needs updating)
- if python3 -m pip install --break-system-packages --upgrade --no-cache-dir .; then
+ # Calculate version from git for setuptools_scm
+ if [ -d .git ]; then
+ git fetch --tags 2>/dev/null || true
+ GIT_VERSION=$(python3 -m setuptools_scm 2>/dev/null || echo "1.0.5")
+ export SETUPTOOLS_SCM_PRETEND_VERSION="$GIT_VERSION"
+ echo "Upgrading to version: $GIT_VERSION"
+ else
+ export SETUPTOOLS_SCM_PRETEND_VERSION="1.0.5"
+ fi
+
+ # Force reinstall the package and all dependencies for clean upgrade
+ if python3 -m pip install --break-system-packages --force-reinstall --no-cache-dir --ignore-installed .; then
echo ""
echo "✓ Package and dependencies updated successfully!"
else
@@ -474,28 +499,10 @@ EOF
echo "⚠ Package update failed, but continuing..."
fi
- # Force reinstall pymc_core to ensure it's always updated
- # Extract the pymc_core dependency from pyproject.toml
+ # Note: pymc_core is already reinstalled as part of the full --force-reinstall above
echo ""
- echo "Ensuring pymc_core is up to date..."
- PYMC_CORE_DEP=$(grep -oP '"pymc_core\[hardware\][^"]*"' pyproject.toml 2>/dev/null | tr -d '"' || echo "")
- if [ -n "$PYMC_CORE_DEP" ]; then
- # Check if it's a Git URL (contains @)
- if [[ "$PYMC_CORE_DEP" == *" @ "* ]]; then
- # Extract just the URL part after " @ "
- PYMC_CORE_SPEC="${PYMC_CORE_DEP#* @ }"
- else
- # Just the package name, use as-is
- PYMC_CORE_SPEC="$PYMC_CORE_DEP"
- fi
- if python3 -m pip install --break-system-packages --force-reinstall --no-cache-dir --no-deps "$PYMC_CORE_SPEC"; then
- echo "✓ pymc_core updated successfully!"
- else
- echo "⚠ pymc_core update failed, but continuing..."
- fi
- else
- echo "⚠ Could not find pymc_core dependency in pyproject.toml"
- fi
+ echo "✓ All packages including pymc_core reinstalled successfully"
+
echo "[8/9] Starting service..."
systemctl daemon-reload
diff --git a/pyproject.toml b/pyproject.toml
index 9c82445..075c7e8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,6 +38,7 @@ dependencies = [
"cherrypy-cors==1.7.0",
"psutil>=5.9.0",
"pyjwt>=2.8.0",
+ "ws4py>=0.6.0",
]
diff --git a/repeater/data_acquisition/letsmesh_handler.py b/repeater/data_acquisition/letsmesh_handler.py
index bcb2dd7..ce8ef79 100644
--- a/repeater/data_acquisition/letsmesh_handler.py
+++ b/repeater/data_acquisition/letsmesh_handler.py
@@ -72,6 +72,9 @@ class _BrokerConnection:
self._connect_time = None
self._tls_verified = False
self._running = False
+ self._reconnect_attempts = 0
+ self._reconnect_timer = None
+ self._max_reconnect_delay = 300 # 5 minutes max
# MQTT WebSocket client - unique client ID per broker
client_id = f"meshcore_{self.public_key}_{broker['host']}"
@@ -118,18 +121,52 @@ class _BrokerConnection:
if rc == 0:
logging.info(f"Connected to {self.broker['name']}")
self._running = True
+ self._reconnect_attempts = 0 # Reset counter on success
if self._on_connect_callback:
self._on_connect_callback(self.broker["name"])
else:
logging.error(f"Failed to connect to {self.broker['name']} (rc={rc})")
+ self._schedule_reconnect()
def _on_disconnect(self, client, userdata, rc):
"""MQTT disconnection callback"""
- logging.warning(f"Disconnected from {self.broker['name']} (rc={rc})")
+ was_running = self._running
self._running = False
+
+ if rc != 0: # Unexpected disconnect
+ logging.warning(f"Disconnected from {self.broker['name']} (rc={rc})")
+ if was_running: # Only reconnect if we were intentionally connected
+ self._schedule_reconnect()
+ else:
+ logging.info(f"Clean disconnect from {self.broker['name']}")
+
if self._on_disconnect_callback:
self._on_disconnect_callback(self.broker["name"])
+ def _schedule_reconnect(self):
+ """Schedule reconnection with exponential backoff"""
+ if self._reconnect_timer:
+ self._reconnect_timer.cancel()
+
+ # Exponential backoff: 5s, 10s, 20s, 40s, 80s, up to max
+ delay = min(5 * (2 ** self._reconnect_attempts), self._max_reconnect_delay)
+ self._reconnect_attempts += 1
+
+ logging.info(f"Scheduling reconnect to {self.broker['name']} in {delay}s (attempt {self._reconnect_attempts})")
+ self._reconnect_timer = threading.Timer(delay, self._attempt_reconnect)
+ self._reconnect_timer.daemon = True
+ self._reconnect_timer.start()
+
+ def _attempt_reconnect(self):
+ """Attempt to reconnect to broker"""
+ try:
+ logging.info(f"Attempting reconnection to {self.broker['name']}...")
+ self.refresh_jwt_token() # Refresh token before reconnecting
+ self.client.reconnect()
+ except Exception as e:
+ logging.error(f"Reconnection failed for {self.broker['name']}: {e}")
+ self._schedule_reconnect() # Try again later
+
def refresh_jwt_token(self):
"""Refresh JWT token for MQTT authentication"""
token = self._generate_jwt()
@@ -165,6 +202,12 @@ class _BrokerConnection:
def disconnect(self):
"""Disconnect from broker"""
self._running = False
+
+ # Cancel any pending reconnection
+ if self._reconnect_timer:
+ self._reconnect_timer.cancel()
+ self._reconnect_timer = None
+
self.client.loop_stop()
self.client.disconnect()
logging.info(f"Disconnected from {self.broker['name']}")
diff --git a/repeater/data_acquisition/sqlite_handler.py b/repeater/data_acquisition/sqlite_handler.py
index 524f180..205b43d 100644
--- a/repeater/data_acquisition/sqlite_handler.py
+++ b/repeater/data_acquisition/sqlite_handler.py
@@ -766,22 +766,31 @@ class SQLiteHandler:
logger.error(f"Failed to get neighbors: {e}")
return {}
- def get_noise_floor_history(self, hours: int = 24) -> list:
+ def get_noise_floor_history(self, hours: int = 24, limit: int = None) -> list:
try:
cutoff = time.time() - (hours * 3600)
with sqlite3.connect(self.sqlite_path) as conn:
conn.row_factory = sqlite3.Row
- measurements = conn.execute("""
+ # Build query with optional limit
+ query = """
SELECT timestamp, noise_floor_dbm
FROM noise_floor
WHERE timestamp > ?
- ORDER BY timestamp ASC
- """, (cutoff,)).fetchall()
+ ORDER BY timestamp DESC
+ """
- return [{"timestamp": row["timestamp"], "noise_floor_dbm": row["noise_floor_dbm"]}
- for row in measurements]
+ if limit:
+ query += f" LIMIT {int(limit)}"
+
+ measurements = conn.execute(query, (cutoff,)).fetchall()
+
+ # Reverse to get chronological order (oldest to newest)
+ result = [{"timestamp": row["timestamp"], "noise_floor_dbm": row["noise_floor_dbm"]}
+ for row in reversed(measurements)]
+
+ return result
except Exception as e:
logger.error(f"Failed to get noise floor history: {e}")
diff --git a/repeater/data_acquisition/storage_collector.py b/repeater/data_acquisition/storage_collector.py
index ec092fc..a2ccee2 100644
--- a/repeater/data_acquisition/storage_collector.py
+++ b/repeater/data_acquisition/storage_collector.py
@@ -66,18 +66,62 @@ class StorageCollector:
from .hardware_stats import HardwareStatsCollector
self.hardware_stats = HardwareStatsCollector()
logger.info("Hardware stats collector initialized")
+
+ # Initialize WebSocket handler for real-time updates
+ self.websocket_available = False
+ try:
+ from .websocket_handler import broadcast_packet, broadcast_stats
+ self.websocket_broadcast_packet = broadcast_packet
+ self.websocket_broadcast_stats = broadcast_stats
+ self.websocket_available = True
+ logger.info("WebSocket handler initialized for real-time updates")
+ except ImportError:
+ logger.debug("WebSocket handler not available")
def _get_live_stats(self) -> dict:
"""Get live stats from RepeaterHandler"""
if not self.repeater_handler:
- return {"uptime_secs": 0, "packets_sent": 0, "packets_received": 0}
+ return {
+ "uptime_secs": 0,
+ "packets_sent": 0,
+ "packets_received": 0,
+ "errors": 0,
+ "queue_len": 0
+ }
uptime_secs = int(time.time() - self.repeater_handler.start_time)
- return {
+
+ # Get airtime stats
+ airtime_stats = self.repeater_handler.airtime_mgr.get_stats()
+
+ # Get latest noise floor from database
+ noise_floor = None
+ try:
+ recent_noise = self.sqlite_handler.get_noise_floor_history(hours=0.5, limit=1)
+ if recent_noise and len(recent_noise) > 0:
+ noise_floor = recent_noise[-1].get('noise_floor_dbm')
+ except Exception as e:
+ logger.debug(f"Could not fetch noise floor: {e}")
+
+ stats = {
"uptime_secs": uptime_secs,
"packets_sent": self.repeater_handler.forwarded_count,
"packets_received": self.repeater_handler.rx_count,
+ "errors": 0,
+ "queue_len": 0, # N/A for Python repeater
}
+
+ # Add airtime stats
+ if airtime_stats:
+ stats["tx_air_secs"] = airtime_stats["total_airtime_ms"] / 1000
+ stats["current_airtime_ms"] = airtime_stats["current_airtime_ms"]
+ stats["utilization_percent"] = airtime_stats["utilization_percent"]
+
+ # Add noise floor if available
+ if noise_floor is not None:
+ stats["noise_floor"] = noise_floor
+
+ return stats
def record_packet(self, packet_record: dict, skip_letsmesh_if_invalid: bool = True):
"""Record packet to storage and publish to MQTT/LetsMesh
@@ -96,6 +140,26 @@ class StorageCollector:
cumulative_counts = self.sqlite_handler.get_cumulative_counts()
self.rrd_handler.update_packet_metrics(packet_record, cumulative_counts)
self.mqtt_handler.publish(packet_record, "packet")
+
+ # Broadcast to WebSocket clients for real-time updates
+ if self.websocket_available:
+ try:
+ self.websocket_broadcast_packet(packet_record)
+
+ # Also broadcast lightweight stats update
+ uptime_seconds = time.time() - self.repeater_handler.start_time if self.repeater_handler else 0
+ self.websocket_broadcast_stats({
+ "packet_stats": {
+ "total_packets": cumulative_counts.get("rx_total", 0),
+ "transmitted_packets": cumulative_counts.get("tx_total", 0),
+ "dropped_packets": cumulative_counts.get("drop_total", 0),
+ },
+ "system_stats": {
+ "uptime_seconds": uptime_seconds,
+ }
+ })
+ except Exception as e:
+ logger.debug(f"WebSocket broadcast failed: {e}")
# Publish to LetsMesh if enabled (skip invalid packets if requested)
if skip_letsmesh_if_invalid and packet_record.get('drop_reason'):
@@ -209,8 +273,8 @@ class StorageCollector:
def cleanup_old_data(self, days: int = 7):
self.sqlite_handler.cleanup_old_data(days)
- def get_noise_floor_history(self, hours: int = 24) -> list:
- return self.sqlite_handler.get_noise_floor_history(hours)
+ def get_noise_floor_history(self, hours: int = 24, limit: int = None) -> list:
+ return self.sqlite_handler.get_noise_floor_history(hours, limit)
def get_noise_floor_stats(self, hours: int = 24) -> dict:
return self.sqlite_handler.get_noise_floor_stats(hours)
diff --git a/repeater/data_acquisition/websocket_handler.py b/repeater/data_acquisition/websocket_handler.py
new file mode 100644
index 0000000..49a2806
--- /dev/null
+++ b/repeater/data_acquisition/websocket_handler.py
@@ -0,0 +1,115 @@
+"""
+WebSocket handler for real-time packet updates - simple ws4py implementation
+"""
+import json
+import logging
+import threading
+import time
+import cherrypy
+from ws4py.websocket import WebSocket
+from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
+
+logger = logging.getLogger("WebSocket")
+
+# Suppress noisy ws4py error logs for normal disconnections (ConnectionResetError, etc.)
+logging.getLogger('ws4py').setLevel(logging.CRITICAL)
+
+# Global set of connected clients
+_connected_clients = set()
+
+# Heartbeat configuration
+PING_INTERVAL = 30 # seconds
+_heartbeat_thread = None
+_heartbeat_running = False
+
+
+class PacketWebSocket(WebSocket):
+
+ def opened(self):
+ """Called when a WebSocket connection is established"""
+ _connected_clients.add(self)
+ logger.info(f"WebSocket connected. Total clients: {len(_connected_clients)}")
+
+ def closed(self, code, reason=None):
+ """Called when a WebSocket connection is closed"""
+ _connected_clients.discard(self)
+ logger.info(f"WebSocket disconnected. Total clients: {len(_connected_clients)}")
+
+ def received_message(self, message):
+ """Handle messages from client"""
+ try:
+ data = json.loads(str(message))
+ if data.get("type") == "ping":
+ self.send(json.dumps({"type": "pong"}))
+ elif data.get("type") == "pong":
+ # Client responded to our ping
+ pass
+ except Exception:
+ pass
+
+
+def broadcast_packet(packet_data: dict):
+
+ if not _connected_clients:
+ return
+
+ message = json.dumps({"type": "packet", "data": packet_data})
+
+ for client in list(_connected_clients):
+ try:
+ client.send(message)
+ except Exception as e:
+ logger.error(f"WebSocket send error: {e}")
+ _connected_clients.discard(client)
+
+
+def broadcast_stats(stats_data: dict):
+
+ if not _connected_clients:
+ return
+
+ message = json.dumps({"type": "stats", "data": stats_data})
+
+ for client in list(_connected_clients):
+ try:
+ client.send(message)
+ except Exception as e:
+ logger.error(f"WebSocket send error: {e}")
+ _connected_clients.discard(client)
+
+
+def _heartbeat_loop():
+ """Background thread to send periodic pings to all connected clients"""
+ global _heartbeat_running
+
+ while _heartbeat_running:
+ time.sleep(PING_INTERVAL)
+
+ if not _connected_clients:
+ continue
+
+ ping_message = json.dumps({"type": "ping"})
+
+ for client in list(_connected_clients):
+ try:
+ client.send(ping_message)
+ except Exception as e:
+ logger.debug(f"Heartbeat ping failed: {e}")
+ _connected_clients.discard(client)
+
+
+def init_websocket():
+ """Initialize WebSocket plugin and start heartbeat"""
+ global _heartbeat_thread, _heartbeat_running
+
+ WebSocketPlugin(cherrypy.engine).subscribe()
+ cherrypy.tools.websocket = WebSocketTool()
+
+ # Start heartbeat thread
+ if not _heartbeat_running:
+ _heartbeat_running = True
+ _heartbeat_thread = threading.Thread(target=_heartbeat_loop, daemon=True)
+ _heartbeat_thread.start()
+ logger.info(f"WebSocket initialized with {PING_INTERVAL}s heartbeat")
+ else:
+ logger.info("WebSocket initialized")
diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py
index 42db631..4de7a65 100644
--- a/repeater/web/api_endpoints.py
+++ b/repeater/web/api_endpoints.py
@@ -1270,12 +1270,13 @@ class APIEndpoints:
@cherrypy.expose
@cherrypy.tools.json_out()
- def noise_floor_history(self, hours: int = 24):
+ def noise_floor_history(self, hours: int = 24, limit: int = None):
try:
storage = self._get_storage()
hours = int(hours)
- history = storage.get_noise_floor_history(hours=hours)
+ limit = int(limit) if limit else None
+ history = storage.get_noise_floor_history(hours=hours, limit=limit)
return self._success({
"history": history,
diff --git a/repeater/web/auth/cherrypy_tool.py b/repeater/web/auth/cherrypy_tool.py
index bbe646f..c6894df 100644
--- a/repeater/web/auth/cherrypy_tool.py
+++ b/repeater/web/auth/cherrypy_tool.py
@@ -8,7 +8,8 @@ def check_auth():
"""
CherryPy tool to check authentication before processing request.
- Checks for either JWT in Authorization header or API token in X-API-Key header.
+ Checks for either JWT in Authorization header, API token in X-API-Key header,
+ or JWT token in query parameter (for EventSource/SSE connections).
Sets cherrypy.request.user on success.
Returns 401 JSON response on failure.
"""
@@ -29,7 +30,7 @@ def check_auth():
cherrypy.response.status = 500
return {"success": False, "error": "Authentication system not configured"}
- # Check for JWT token first
+ # Check for JWT token in Authorization header first
auth_header = cherrypy.request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:] # Remove "Bearer " prefix
@@ -43,7 +44,23 @@ def check_auth():
}
return
- # Check for API token
+ # Check for JWT token in query parameter (for EventSource/SSE)
+ # EventSource doesn't support custom headers, so we use query param
+ query_token = cherrypy.request.params.get("token")
+ if query_token:
+ payload = jwt_handler.verify_jwt(query_token)
+
+ if payload:
+ cherrypy.request.user = {
+ "username": payload.get("sub"),
+ "client_id": payload.get("client_id"),
+ "auth_type": "jwt_query"
+ }
+ # Remove token from params to avoid exposing it in logs
+ del cherrypy.request.params["token"]
+ return
+
+ # Check for API token in X-API-Key header
api_key = cherrypy.request.headers.get("X-API-Key", "")
if api_key:
token_info = token_manager.verify_token(api_key)
diff --git a/repeater/web/html/assets/CADCalibration-DnmufMQ0.css b/repeater/web/html/assets/CADCalibration-DnmufMQ0.css
new file mode 100644
index 0000000..d158fa0
--- /dev/null
+++ b/repeater/web/html/assets/CADCalibration-DnmufMQ0.css
@@ -0,0 +1 @@
+.glass-card[data-v-c30e5f38]{background:var(--color-glass-bg);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid var(--color-glass-border);box-shadow:var(--color-glass-shadow)}
diff --git a/repeater/web/html/assets/CADCalibration-sfiSWhAM.js b/repeater/web/html/assets/CADCalibration-sfiSWhAM.js
new file mode 100644
index 0000000..a7cd9ca
--- /dev/null
+++ b/repeater/web/html/assets/CADCalibration-sfiSWhAM.js
@@ -0,0 +1 @@
+import{a as G,M as K,c as W,r as o,o as X,Q as Y,b as g,e as a,g as k,i as F,t as l,k as h,n as ee,L as T,Z as te,$ as ae,p as f,x as se}from"./index-C2DY4pTz.js";import{P as M}from"./plotly.min-DO11Gp-n.js";import"./_commonjsHelpers-CqkleIqs.js";const oe={class:"p-6 space-y-6"},re={class:"glass-card rounded-[15px] p-6"},le={class:"flex justify-center"},ne={class:"flex gap-4"},ie=["disabled"],ce=["disabled"],de={class:"glass-card rounded-[15px] p-6 space-y-4"},ue={class:"text-content-primary dark:text-content-primary"},ve={key:0,class:"p-4 bg-primary/10 border border-primary/30 rounded-lg"},pe={class:"text-content-primary dark:text-primary"},me={class:"space-y-2"},be={class:"w-full bg-white/10 rounded-full h-2"},ge={class:"text-content-secondary dark:text-content-muted text-sm"},fe={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},xe={class:"glass-card rounded-[15px] p-4 text-center"},ye={class:"text-2xl font-bold text-primary"},_e={class:"glass-card rounded-[15px] p-4 text-center"},ke={class:"text-2xl font-bold text-primary"},he={class:"glass-card rounded-[15px] p-4 text-center"},Ce={class:"text-2xl font-bold text-primary"},we={class:"glass-card rounded-[15px] p-4 text-center"},Re={class:"text-2xl font-bold text-primary"},Se={key:0,class:"glass-card rounded-[15px] p-6 space-y-4"},De={key:0,class:"p-4 bg-accent-green/10 border border-accent-green/30 rounded-lg"},Ae={class:"text-content-primary dark:text-content-primary mb-4"},Be={key:1,class:"p-4 bg-secondary/20 border border-secondary/40 rounded-lg"},Ee=G({name:"CADCalibrationView",__name:"CADCalibration",setup(Fe){const m=K(),$=W(()=>document.documentElement.classList.contains("dark")),I=()=>{const e=$.value;return{title:e?"#F9FAFB":"#111827",subtitle:e?"#9CA3AF":"#6B7280",axis:e?"#D1D5DB":"#374151",tick:e?"#9CA3AF":"#6B7280",grid:e?"rgba(148, 163, 184, 0.1)":"rgba(107, 114, 128, 0.15)",zeroline:e?"rgba(148, 163, 184, 0.2)":"rgba(107, 114, 128, 0.25)",line:e?"rgba(148, 163, 184, 0.3)":"rgba(107, 114, 128, 0.35)",colorbarBorder:e?"rgba(255,255,255,0.2)":"rgba(0,0,0,0.15)",markerLine:e?"rgba(255,255,255,0.2)":"rgba(0,0,0,0.15)"}},u=o(!1),C=o(null),r=o(null),v=o({}),n=o(null),P=o([]),N=o({}),d=o("Ready to start calibration"),x=o(0),b=o(0),w=o(0),R=o(0),S=o(0),D=o(0),i=o(null),A=o(!1),B=o(!1),y=o(!1),_=o(!1);let c=null;const O={responsive:!0,displayModeBar:!0,modeBarButtonsToRemove:["pan2d","select2d","lasso2d","autoScale2d"],displaylogo:!1,toImageButtonOptions:{format:"png",filename:"cad-calibration-heatmap",height:600,width:800,scale:2}};function V(){const e=I(),t=[{x:[],y:[],z:[],mode:"markers",type:"scatter",marker:{size:12,color:[],colorscale:[[0,"rgba(75, 85, 99, 0.4)"],[.1,"rgba(6, 182, 212, 0.3)"],[.5,"rgba(6, 182, 212, 0.6)"],[1,"rgba(16, 185, 129, 0.9)"]],showscale:!0,colorbar:{title:{text:"Detection Rate (%)",font:{color:e.axis,size:14}},tickfont:{color:e.tick},bgcolor:"rgba(0,0,0,0)",bordercolor:e.colorbarBorder,borderwidth:1,thickness:15},line:{color:e.markerLine,width:1}},hovertemplate:"Peak: %{x}
Min: %{y}
Detection Rate: %{marker.color:.1f}%
Channel Activity Detection Calibration`,font:{color:e.title,size:18},x:.5},xaxis:{title:{text:"CAD Peak Threshold",font:{color:e.axis,size:14}},tickfont:{color:e.tick},gridcolor:e.grid,zerolinecolor:e.zeroline,linecolor:e.line},yaxis:{title:{text:"CAD Min Threshold",font:{color:e.axis,size:14}},tickfont:{color:e.tick},gridcolor:e.grid,zerolinecolor:e.zeroline,linecolor:e.line},plot_bgcolor:"rgba(0, 0, 0, 0)",paper_bgcolor:"rgba(0, 0, 0, 0)",font:{color:e.title,family:"Inter, system-ui, sans-serif"},margin:{l:80,r:80,t:100,b:80},showlegend:!1};M.newPlot("plotly-chart",t,s,O)}function j(){if(Object.keys(v.value).length===0)return;const e=Object.values(v.value),t=[],s=[],p=[];for(const E of e)t.push(E.det_peak),s.push(E.det_min),p.push(E.detection_rate);const q={x:[t],y:[s],"marker.color":[p],hovertemplate:"Peak: %{x}
Min: %{y}
Detection Rate: %{marker.color:.1f}%
Status: Tested
Permit flooding
Block flooding
Permit flooding
Block flooding
API tokens are used for machine-to-machine authentication. Include the token in the X-API-Key header when making API requests.
Tokens are only shown once at creation. Store them securely.
PyMC Console must be installed at /opt/pymc_console/web/html before selecting this option.
Web frontend changes will take effect after restarting the pymc-repeater service.
Activity (Last 24 Hours)
Access documentation, setup guides, troubleshooting tips, and community resources on our official wiki.
Visit Wiki DocumentationSign in to access your dashboard
No logs match the current filter criteria.
',3)]))):(n(),s("div",ie,[(n(!0),s(L,null,N(_.value,(r,l)=>(n(),s("div",{key:l,class:"flex items-start gap-4 p-4 hover:bg-background-mute dark:hover:bg-stroke/5 transition-colors font-mono text-sm"},[o("span",ue," ["+c(O(r.timestamp))+"] ",1),o("span",ge,c(f(r.message)),1),o("span",{class:h(["flex-shrink-0 px-2 py-1 text-xs font-medium rounded",B(r.level)])},c(r.level),3),o("span",be,c(S(r.message)),1)]))),128))]))]))])]))}});export{ve as default}; diff --git a/repeater/web/html/assets/Neighbors-BhwSlX3P.js b/repeater/web/html/assets/Neighbors-BhwSlX3P.js new file mode 100644 index 0000000..b89fca9 --- /dev/null +++ b/repeater/web/html/assets/Neighbors-BhwSlX3P.js @@ -0,0 +1,65 @@ +import{a as bt,b as $,g as D,e as t,t as C,s as Lt,p as f,M as Yt,r as F,c as J,D as ht,N as zt,f as it,T as Ft,l as Dt,O as jt,j as M,F as ct,h as gt,x as It,k as tt,o as Xt,Q as te,i as ft,E as Pt,n as At,w as wt,R as ie,q as Wt,v as le,L as Et}from"./index-C2DY4pTz.js";import{u as Ut}from"./useSignalQuality-D9wfbwdb.js";import{L as W}from"./leaflet-src-BtisrQHC.js";/* empty css */import{g as _t,s as Ct}from"./preferences-DtwbSSgO.js";import"./_commonjsHelpers-CqkleIqs.js";const de={class:"bg-gray-50 dark:bg-white/5 border border-stroke-subtle dark:border-stroke/10 rounded-lg p-4 mb-6"},ce={class:"flex items-center gap-3"},ue={class:"flex-1 min-w-0"},pe={class:"text-content-primary dark:text-content-primary font-medium truncate"},ge={class:"text-content-secondary dark:text-content-muted text-sm font-mono"},me={key:0,class:"text-white/50 text-xs"},he={key:1,class:"text-white/50 text-xs"},be=bt({__name:"DeleteNeighborModal",props:{show:{type:Boolean},neighbor:{}},emits:["close","delete"],setup(A,{emit:o}){const r=A,i=o,e=()=>{r.neighbor&&(i("delete",r.neighbor.id),d())},d=()=>{i("close")},g=s=>{s.target===s.currentTarget&&d()};return(s,a)=>s.show&&s.neighbor?(f(),$("div",{key:0,onClick:g,class:"fixed inset-0 bg-black/80 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:a[0]||(a[0]=Lt(()=>{},["stop"]))},[t("div",{class:"flex items-center gap-3 mb-6"},[a[2]||(a[2]=t("svg",{class:"w-6 h-6 text-accent-red",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[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-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"})],-1)),a[3]||(a[3]=t("div",null,[t("h3",{class:"text-xl font-semibold text-content-primary dark:text-content-primary"},"Delete Neighbor"),t("p",{class:"text-content-secondary dark:text-content-muted text-sm mt-1"}," Are you sure you want to delete this neighbor? ")],-1)),t("button",{onClick:d,class:"ml-auto text-content-secondary dark:text-content-muted hover:text-content-primary dark:hover:text-content-primary transition-colors"},a[1]||(a[1]=[t("svg",{class:"w-5 h-5",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",de,[t("div",ce,[t("div",ue,[t("div",pe,C(s.neighbor?.node_name||s.neighbor?.long_name||s.neighbor?.short_name||"Unknown"),1),t("div",ge," ID: "+C(s.neighbor?.node_num_hex||s.neighbor?.node_num||s.neighbor?.id||"N/A"),1),s.neighbor?.contact_type?(f(),$("div",me,C(s.neighbor.contact_type),1)):D("",!0),s.neighbor?.hw_model?(f(),$("div",he,C(s.neighbor.hw_model),1)):D("",!0)])])]),a[4]||(a[4]=t("div",{class:"bg-accent-red/10 border border-accent-red/30 rounded-lg p-4 mb-6"},[t("div",{class:"flex items-center gap-2 text-accent-red text-sm"},[t("svg",{class:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"})]),t("span",null,"This action cannot be undone")])],-1)),t("div",{class:"flex gap-3"},[t("button",{onClick:d,class:"flex-1 px-4 py-3 bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 border border-stroke-subtle dark:border-stroke/20 text-content-primary dark:text-content-primary rounded-lg transition-colors"}," Cancel "),t("button",{onClick:e,class:"flex-1 px-4 py-3 bg-accent-red/20 hover:bg-accent-red/30 border border-accent-red/50 text-accent-red rounded-lg transition-colors font-medium"}," Delete ")])])])):D("",!0)}}),xe={class:"bg-gradient-to-r from-primary/20 to-accent-blue/20 border-b border-stroke-subtle dark:border-stroke/10 px-6 py-4"},ve={class:"flex items-center justify-between"},ye={class:"flex items-center gap-3"},ke={key:0,class:"text-sm text-content-secondary dark:text-content-muted"},fe={class:"p-6"},we={key:0,class:"text-center py-8"},_e={key:1,class:"text-center py-8"},Ce={class:"text-content-secondary dark:text-content-muted text-sm"},$e={key:2,class:"space-y-4"},Me={class:"bg-background-mute dark:bg-background/50 border border-stroke-subtle dark:border-stroke/10 rounded-[15px] p-4"},Ae={class:"flex items-center justify-between mb-2"},Le={class:"flex items-baseline gap-2"},Te={class:"text-3xl font-bold text-content-primary dark:text-content-primary"},Ee={class:"grid grid-cols-2 gap-3"},Se={class:"bg-background-mute dark:bg-background/50 border border-stroke-subtle dark:border-stroke/10 rounded-[15px] p-4"},Be={class:"flex items-center gap-2 mb-2"},Ne={class:"flex gap-0.5"},Fe={class:"flex items-baseline gap-1"},De={class:"text-xl font-bold text-content-primary dark:text-content-primary"},Pe={class:"bg-background-mute dark:bg-background/50 border border-stroke-subtle dark:border-stroke/10 rounded-[15px] p-4"},Re={class:"flex items-baseline gap-1"},ze={class:"text-xl font-bold text-content-primary dark:text-content-primary"},je={class:"bg-background-mute dark:bg-background/50 border border-stroke-subtle dark:border-stroke/10 rounded-[15px] p-4"},Ie={class:"relative"},Ue={class:"flex items-center gap-2 overflow-x-auto pb-2"},Oe={key:0,class:"relative flex items-center"},Ve={key:0,class:"absolute left-1/2 -translate-x-1/2 animate-pulse"},He={class:"text-content-muted dark:text-content-muted text-xs mt-2 flex items-center justify-between"},Ze={key:0,class:"text-cyan-500 dark:text-primary animate-pulse"},We={class:"flex items-center justify-between text-xs text-content-muted dark:text-content-muted pt-2"},Qe=bt({__name:"PingResultModal",props:{show:{type:Boolean},nodeName:{default:null},result:{default:null},error:{default:null},loading:{type:Boolean,default:!1}},emits:["close"],setup(A,{emit:o}){const r=A,i=o,e=Yt(),{getSignalQuality:d}=Ut(),g=F(0),s=F(!1),a=J(()=>{const x=e.stats?.config?.radio?.spreading_factor??7,b=e.stats?.config?.radio?.bandwidth??125,L=e.stats?.config?.radio?.coding_rate??5,_=Math.pow(2,x)/b,k=8+4.25*(L-4)+20;return _*k}),w=J(()=>{if(!r.result)return{color:"text-gray-400",label:"Unknown"};const x=r.result.rtt_ms,b=a.value,L=r.result.path.length,k=2*b*L+500*L;return xNo valid coordinates available
Configure base station location to view map
No mesh neighbors have been discovered in your area yet.
',3)),t("button",{onClick:E,class:"mt-4 px-4 py-2 bg-primary/20 text-primary border border-primary/30 rounded-lg hover:bg-primary/30 transition-colors"}," Refresh ")])):at.value.length===0&&et.value?(f(),$("div",fn,[p[21]||(p[21]=ft('Try adjusting your filter criteria to see more results.
',3)),t("button",{onClick:K,class:"px-4 py-2 bg-primary/20 text-primary border border-primary/30 rounded-lg hover:bg-primary/30 transition-colors"}," Clear Filters ")])):D("",!0)],64)),it(be,{show:y.value,neighbor:Z.value,onClose:R,onDelete:H},null,8,["show","neighbor"]),it(qe,{show:S.value,"node-name":_.value,result:b.value,error:L.value,loading:x.value,onClose:z},null,8,["show","node-name","result","error","loading"]),it(Po,{"is-open":P.value,neighbor:I.value,"base-latitude":v.value,"base-longitude":u.value,onClose:X},null,8,["is-open","neighbor","base-latitude","base-longitude"])]))}});export{Ln as default}; diff --git a/repeater/web/html/assets/Neighbors-Dm-0E9wE.css b/repeater/web/html/assets/Neighbors-Dm-0E9wE.css new file mode 100644 index 0000000..59fe5a8 --- /dev/null +++ b/repeater/web/html/assets/Neighbors-Dm-0E9wE.css @@ -0,0 +1 @@ +.modal-enter-active[data-v-bea9143c],.modal-leave-active[data-v-bea9143c]{transition:opacity .2s ease}.modal-enter-from[data-v-bea9143c],.modal-leave-to[data-v-bea9143c]{opacity:0}.modal-enter-active>div[data-v-bea9143c],.modal-leave-active>div[data-v-bea9143c]{transition:transform .2s ease}.modal-enter-from>div[data-v-bea9143c],.modal-leave-to>div[data-v-bea9143c]{transform:scale(.95)}.packet-enter-active[data-v-bea9143c],.packet-leave-active[data-v-bea9143c]{transition:all .15s ease}.packet-enter-from[data-v-bea9143c],.packet-leave-to[data-v-bea9143c]{opacity:0;transform:translate(-50%) scale(.5)}.custom-scrollbar[data-v-cbe6bf60]::-webkit-scrollbar{width:8px}.custom-scrollbar[data-v-cbe6bf60]::-webkit-scrollbar-track{background:transparent}.custom-scrollbar[data-v-cbe6bf60]::-webkit-scrollbar-thumb{background:#0003;border-radius:4px}.dark .custom-scrollbar[data-v-cbe6bf60]::-webkit-scrollbar-thumb{background:#fff3}.custom-scrollbar[data-v-cbe6bf60]::-webkit-scrollbar-thumb:hover{background:#0000004d}.dark .custom-scrollbar[data-v-cbe6bf60]::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.modal-enter-active[data-v-cbe6bf60],.modal-leave-active[data-v-cbe6bf60]{transition:opacity .3s ease}.modal-enter-active>div[data-v-cbe6bf60],.modal-leave-active>div[data-v-cbe6bf60]{transition:transform .3s ease,opacity .3s ease}.modal-enter-from[data-v-cbe6bf60],.modal-leave-to[data-v-cbe6bf60]{opacity:0}.modal-enter-from>div[data-v-cbe6bf60],.modal-leave-to>div[data-v-cbe6bf60]{transform:scale(.95);opacity:0}.leaflet-container{background:transparent}.custom-marker{background:transparent!important;border:none!important}.map-container[data-v-a6a23e33]{position:relative;background:transparent;border-radius:15px;overflow:hidden}.leaflet-map-container[data-v-a6a23e33]{background:linear-gradient(135deg,#09090bcc,#0009);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px)}.map-legend[data-v-a6a23e33]{position:absolute;top:10px;right:10px;background:#0006;border:1px solid rgba(255,255,255,.1);border-radius:15px;padding:12px;font-size:12px;color:#fff;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);z-index:1000;min-width:150px;max-width:180px;box-shadow:0 8px 32px #0000004d}.legend-title[data-v-a6a23e33]{font-weight:700;margin-bottom:10px;color:#fff;font-size:13px}.legend-section[data-v-a6a23e33]{margin-bottom:10px}.legend-section[data-v-a6a23e33]:last-of-type{margin-bottom:8px}.legend-subtitle[data-v-a6a23e33]{font-weight:600;margin-bottom:6px;color:#fffc;font-size:11px;text-transform:uppercase;letter-spacing:.5px}.legend-footer[data-v-a6a23e33]{margin-top:10px;padding-top:8px;border-top:1px solid rgba(255,255,255,.1);color:#fff9;font-size:10px;text-align:center}.legend-items[data-v-a6a23e33]{display:flex;flex-direction:column;gap:4px}.legend-item[data-v-a6a23e33]{display:flex;align-items:center;gap:6px}.legend-icon[data-v-a6a23e33]{width:8px;height:8px;border-radius:50%;border:1px solid rgba(255,255,255,.8);box-shadow:0 1px 2px #0003;flex-shrink:0}.legend-icon.cluster-icon[data-v-a6a23e33]{width:16px;height:16px;border-radius:50%;border:1px solid #AAE8E8;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.legend-line[data-v-a6a23e33]{width:16px;height:2px;border-radius:1px;flex-shrink:0;position:relative}.legend-line-dashed[data-v-a6a23e33]{background-image:repeating-linear-gradient(90deg,currentColor 0px,currentColor 4px,transparent 4px,transparent 8px)!important;background-color:transparent!important}.legend-line-dashed[style*="#FFC246"][data-v-a6a23e33]{color:#ffc246!important}.legend-line-dashed[style*="#ea580c"][data-v-a6a23e33]{color:#ea580c!important}.marker-highlight{position:relative!important;z-index:1000!important;animation:marker-glow-a6a23e33 1s ease-in-out infinite!important;border-radius:50%!important;box-shadow:0 0 0 3px #a5e5b6,0 0 8px #a5e5b6,0 0 16px #a5e5b6!important;transform:scale(1.2)!important}@keyframes marker-glow-a6a23e33{0%,to{box-shadow:0 0 0 3px #a5e5b6,0 0 8px #a5e5b6,0 0 16px #a5e5b6;filter:brightness(1)}50%{box-shadow:0 0 0 5px #a5e5b6,0 0 12px #a5e5b6,0 0 24px #a5e5b6;filter:brightness(1.3)}}@keyframes pulse-highlight-a6a23e33{0%{box-shadow:0 0 #3b82f6b3}70%{box-shadow:0 0 0 8px #3b82f600}to{box-shadow:0 0 #3b82f600}}.leaflet-popup-content-wrapper{background:#0006!important;color:#fff!important;border-radius:15px!important;box-shadow:0 8px 32px #0000004d!important;border:1px solid rgba(255,255,255,.1)!important;-webkit-backdrop-filter:blur(20px)!important;backdrop-filter:blur(20px)!important}.leaflet-popup-tip{background:#0006!important;border:1px solid rgba(255,255,255,.1)!important}.leaflet-popup-close-button{color:#fff9!important;font-size:18px!important}.leaflet-popup-close-button:hover{color:#fff!important}.custom-div-icon,.custom-cluster-icon{background:transparent!important;border:none!important}.custom-cluster-icon div{transition:all .3s ease!important;cursor:pointer!important}.custom-cluster-icon:hover div{transform:scale(1.1)!important;box-shadow:0 6px 16px #aae8e880!important}.leaflet-control-zoom{border:1px solid rgba(255,255,255,.1)!important;border-radius:15px!important;overflow:hidden;-webkit-backdrop-filter:blur(20px)!important;backdrop-filter:blur(20px)!important}.leaflet-control-zoom a{background-color:#0006!important;color:#fff!important;border-bottom:1px solid rgba(255,255,255,.1)!important;transition:all .2s ease!important}.leaflet-control-zoom a:hover{background-color:#ffffff1a!important;color:#fff!important}.leaflet-control-attribution{background-color:#1f2937cc!important;color:#9ca3af!important;border-top:1px solid rgba(75,85,99,.3)!important;border-radius:4px!important;padding:4px 8px!important;font-size:11px!important}.leaflet-control-attribution a{color:#60a5fa!important;text-decoration:none}.leaflet-control-attribution a:hover{color:#93c5fd!important;text-decoration:underline}.leaflet-bottom.leaflet-left .leaflet-control-attribution{margin-left:10px!important;margin-bottom:10px!important}.map-attribution[data-v-a6a23e33]{position:absolute;bottom:10px;left:10px;background:#0006;color:#fff9;border:1px solid rgba(255,255,255,.1);border-radius:15px;padding:4px 8px;font-size:10px;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);z-index:1000}@media (max-width: 640px){.leaflet-control-attribution{display:none!important}} diff --git a/repeater/web/html/assets/RoomServers-IKqFauvg.js b/repeater/web/html/assets/RoomServers-IKqFauvg.js new file mode 100644 index 0000000..aab7443 --- /dev/null +++ b/repeater/web/html/assets/RoomServers-IKqFauvg.js @@ -0,0 +1 @@ +import{a as te,b as s,g as c,e,j as f,t as a,s as q,p as n,r as d,D as xe,o as ge,L as y,f as Z,i as G,k,F as N,h as J,w as v,v as b,Y as ee}from"./index-C2DY4pTz.js";import{g as ye,s as ke}from"./preferences-DtwbSSgO.js";import{_ as fe}from"./ConfirmDialog.vue_vue_type_script_setup_true_lang-BIwbENrM.js";const he={class:"mb-6"},we={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},_e={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},Ce={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},Me={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},je={class:"flex"},Le=te({__name:"MessageDialog",props:{show:{type:Boolean},message:{},variant:{default:"success"}},emits:["close"],setup(W,{emit:B}){const x=W,i=B,M=g=>{g.target===g.currentTarget&&i("close")},j={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"},l={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(g,p)=>x.show?(n(),s("div",{key:0,onClick:M,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"}},[e("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:p[1]||(p[1]=q(()=>{},["stop"]))},[e("div",he,[e("div",{class:f(["inline-flex p-3 rounded-xl mb-4",j[x.variant]])},[x.variant==="success"?(n(),s("svg",we,p[2]||(p[2]=[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"},null,-1)]))):x.variant==="error"?(n(),s("svg",_e,p[3]||(p[3]=[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"},null,-1)]))):(n(),s("svg",Ce,p[4]||(p[4]=[e("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),e("p",Me,a(x.message),1)]),e("div",je,[e("button",{onClick:p[0]||(p[0]=L=>i("close")),class:f(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",l[x.variant]])}," OK ",2)])])])):c("",!0)}}),$e={class:"p-6 space-y-6"},Se={class:"relative overflow-hidden rounded-[20px] p-6 mb-6 glass-card backdrop-blur-xl border border-stroke-subtle dark:border-white/10"},Be={class:"relative flex items-center justify-between"},Ae={key:0,class:"grid grid-cols-1 md:grid-cols-3 gap-4"},Ve={class:"group relative overflow-hidden glass-card backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[15px] p-5 hover:scale-[1.02] transition-all duration-300 cursor-pointer"},Re={class:"relative flex items-center justify-between"},ze={class:"text-3xl font-bold text-content-primary dark:text-content-primary mb-1"},De={class:"group relative overflow-hidden glass-card backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[15px] p-5 hover:scale-[1.02] transition-all duration-300 cursor-pointer"},Ee={class:"relative flex items-center justify-between"},Fe={class:"text-3xl font-bold text-primary mb-1"},Ie={class:"group relative overflow-hidden glass-card backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[15px] p-5 hover:scale-[1.02] transition-all duration-300 cursor-pointer"},Ne={class:"relative flex items-center justify-between"},Ue={key:0,class:"w-6 h-6 text-accent-green",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},He={key:1,class:"w-6 h-6 text-accent-yellow",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},Ke={class:"glass-card backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[15px] p-6"},Oe={key:0,class:"flex items-center justify-center py-12"},Pe={key:1,class:"flex items-center justify-center py-12"},Te={class:"text-center"},Ge={class:"text-content-secondary dark:text-content-muted text-sm mb-4"},Je={key:2,class:"space-y-4"},qe={class:"relative flex items-start justify-between"},We={class:"flex-1"},Ye={class:"flex items-center gap-3 mb-4"},Qe={class:"relative"},Xe={key:0,class:"absolute inset-0 bg-accent-green/50 rounded-full animate-ping"},Ze={class:"text-xl font-bold text-content-primary dark:text-content-primary group-hover:text-primary transition-colors"},et={key:0,class:"text-content-muted dark:text-content-muted text-sm"},tt={class:"grid grid-cols-1 md:grid-cols-2 gap-3 text-sm mb-3"},rt={class:"text-content-primary dark:text-content-primary/90 ml-2"},ot={class:"flex items-center gap-2"},st={key:0,class:"text-content-primary dark:text-content-primary/90 font-mono ml-2 text-xs"},nt={key:1,class:"text-content-muted dark:text-content-muted ml-2 text-xs"},at=["onClick"],lt={class:"text-content-primary dark:text-content-primary/90 ml-2"},dt={key:0},it={class:"text-content-primary dark:text-content-primary/90 ml-2"},ut={key:0,class:"text-accent-green"},ct={key:1,class:"text-content-muted dark:text-content-muted"},pt={key:2,class:"text-primary"},mt={key:0,class:"text-xs text-content-muted dark:text-content-muted font-mono"},vt={class:"ml-4 flex flex-wrap gap-2"},bt=["onClick","disabled","title"],xt=["onClick","disabled","title"],gt=["onClick"],yt=["onClick"],kt={key:3,class:"text-center py-12 text-content-secondary dark:text-content-muted"},ft={key:1,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"},ht={class:"bg-white dark:bg-surface-elevated backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[15px] p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto"},wt={class:"space-y-4"},_t={class:"block text-content-secondary dark:text-content-primary/70 text-sm mb-2"},Ct={key:0},Mt={key:1,class:"text-content-secondary dark:text-content-muted text-sm"},jt={class:"grid grid-cols-2 gap-4"},Lt={class:"grid grid-cols-2 gap-4"},$t={key:2,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"},St={class:"bg-white dark:bg-surface-elevated backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[15px] p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto"},Bt={class:"space-y-4"},At=["value"],Vt={class:"block text-content-secondary dark:text-content-primary/70 text-sm mb-2"},Rt={key:0},zt={key:1,class:"text-content-secondary dark:text-content-muted text-sm"},Dt={class:"grid grid-cols-2 gap-4"},Et={class:"grid grid-cols-2 gap-4"},Ft={key:0,class:"fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4"},It={class:"bg-white dark:bg-surface-elevated backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[20px] p-6 max-w-4xl w-full h-[85vh] flex flex-col shadow-2xl"},Nt={class:"relative overflow-hidden rounded-[15px] mb-6 p-5 bg-white/50 dark:bg-white/5 border border-stroke-subtle dark:border-white/10"},Ut={class:"relative flex items-center justify-between"},Ht={class:"flex items-center gap-4"},Kt={class:"text-content-secondary dark:text-content-muted text-sm flex items-center gap-2"},Ot={class:"text-primary font-semibold"},Pt={class:"flex items-center gap-2"},Tt={class:"bg-primary/30 px-1.5 py-0.5 rounded-full text-[10px]"},Gt={class:"flex-1 overflow-y-auto mb-4 space-y-3"},Jt={key:0,class:"flex items-center justify-center py-12"},qt={key:1,class:"flex items-center justify-center py-12"},Wt={class:"text-center"},Yt={class:"text-content-secondary dark:text-content-muted text-sm mb-4"},Qt={key:2,class:"space-y-3"},Xt={class:"relative flex items-start justify-between gap-3"},Zt={class:"flex-1 min-w-0"},er={class:"flex items-center gap-2 mb-3"},tr={class:"flex items-center gap-2 flex-wrap"},rr={key:0,class:"text-primary text-sm font-bold"},or={key:1,class:"text-primary/80 text-xs font-mono bg-primary/10 px-2 py-1 rounded-md border border-primary/20"},sr={key:2,class:"text-content-muted dark:text-content-muted text-xs"},nr={class:"text-content-secondary dark:text-content-muted text-xs flex items-center gap-1"},ar={key:3,class:"text-content-muted dark:text-content-muted/50 text-[10px] font-mono bg-background-mute dark:bg-white/5 px-1.5 py-0.5 rounded"},lr={class:"text-content-primary dark:text-content-primary/90 text-sm leading-relaxed break-words whitespace-pre-wrap bg-gray-50 dark:bg-white/5 p-3 rounded-[10px] border border-stroke-subtle dark:border-white/5"},dr=["onClick"],ir={key:0,class:"text-center pt-4"},ur={key:1,class:"text-center pt-4"},cr={key:3,class:"flex items-center justify-center h-full"},pr={class:"relative overflow-hidden rounded-[15px] border-t border-stroke-subtle dark:border-white/20 pt-4 mt-4"},mr={class:"relative space-y-3"},vr={class:"flex gap-3"},br={class:"flex-1 relative"},xr=["onKeydown"],gr=["disabled"],yr={key:1,class:"fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-[60] p-4"},kr={class:"bg-white dark:bg-surface-elevated backdrop-blur-xl border border-stroke-subtle dark:border-white/10 rounded-[15px] p-6 max-w-3xl w-full max-h-[80vh] flex flex-col"},fr={class:"flex items-center justify-between mb-4 pb-4 border-b border-stroke-subtle dark:border-white/10"},hr={class:"text-content-secondary dark:text-content-primary/70 text-sm mt-1"},wr={class:"text-primary"},_r={class:"flex-1 overflow-y-auto space-y-3"},Cr={key:0,class:"text-center py-12"},Mr={class:"space-y-2"},jr={class:"flex items-center justify-between"},Lr={class:"flex items-center gap-2"},$r={class:"text-content-primary dark:text-content-primary font-semibold"},Sr={class:"flex items-center gap-2"},Br={class:"text-content-secondary dark:text-content-muted text-xs"},Ar=["onClick"],Vr={class:"space-y-1 text-xs"},Rr={class:"flex items-center gap-2"},zr={class:"text-primary font-mono bg-primary/10 px-2 py-0.5 rounded"},Dr={class:"flex items-center gap-2"},Er={class:"text-primary font-mono bg-primary/10 px-2 py-0.5 rounded text-[10px] break-all"},Fr={class:"flex items-center justify-between text-xs text-content-secondary dark:text-content-muted"},Ir={class:"flex items-center gap-4"},Nr={key:0},Ur={key:1},Hr={key:0},Tr=te({name:"RoomServersView",__name:"RoomServers",setup(W){const B=d(!1),x=d(null),i=d(null),M=d(!1),j=d(!1),l=d(null),g=d(!1),p=d(!1),L=d(new Set),z=d(!1),D=d(""),U=d(!1),H=d({message:"",variant:"success"}),K=d(!1),h=d(""),E=d(""),w=d([]),A=d(!1),$=d(null),_=d(""),F=d(ye("roomServers_messagesLimit",50)),I=d(0),O=d(!0);xe(F,o=>ke("roomServers_messagesLimit",o));const S=d([]),P=d(!1),m=d({name:"",identity_key:"",type:"room_server",settings:{node_name:"",latitude:0,longitude:0,admin_password:"",guest_password:""}});ge(async()=>{await V()});async function V(){B.value=!0,x.value=null;try{const o=await y.getIdentities();o.success?i.value=o.data:x.value=o.error||"Failed to load identities"}catch(o){x.value=o instanceof Error?o.message:"Failed to load identities"}finally{B.value=!1}}async function re(){try{const o=await y.createIdentity(m.value);o.success?(M.value=!1,Y(),await V(),u(o.message||"Identity created successfully!","success")):u(`Failed to create identity: ${o.error}`,"error")}catch(o){u(`Error creating identity: ${o}`,"error")}}async function oe(){try{const o=await y.updateIdentity(l.value);o.success?(j.value=!1,l.value=null,await V(),u(o.message||"Identity updated successfully!","success")):u(`Failed to update identity: ${o.error}`,"error")}catch(o){u(`Error updating identity: ${o}`,"error")}}function se(o){D.value=o,z.value=!0}async function ne(){const o=D.value;z.value=!1;try{const t=await y.deleteIdentity(o);t.success?(await V(),u(t.message||"Identity deleted successfully!","success")):u(`Failed to delete identity: ${t.error}`,"error")}catch(t){u(`Error deleting identity: ${t}`,"error")}finally{D.value=""}}function u(o,t){H.value={message:o,variant:t},U.value=!0}async function ae(o){try{const t=await y.sendRoomServerAdvert(o);t.success?u(t.message||`Advert sent for '${o}'!`,"success"):u(`Failed to send advert: ${t.error}`,"error")}catch(t){u(`Error sending advert: ${t}`,"error")}}function le(o){l.value=JSON.parse(JSON.stringify(o)),l.value.settings||(l.value.settings={}),l.value.settings.admin_password||(l.value.settings.admin_password=""),l.value.settings.guest_password||(l.value.settings.guest_password=""),p.value=!1,j.value=!0}function Y(){m.value={name:"",identity_key:"",type:"room_server",settings:{node_name:"",latitude:0,longitude:0,admin_password:"",guest_password:""}},g.value=!1}function Q(){M.value=!1,j.value=!1,l.value=null,g.value=!1,p.value=!1,Y()}function de(o){L.value.has(o)?L.value.delete(o):L.value.add(o)}async function ie(o){h.value=o,K.value=!0,I.value=0,O.value=!0;const t=i.value?.configured.find(r=>r.name===o);E.value=t?.hash||"",await X(),await R(!0)}async function X(){try{console.log("Fetching ACL clients for room:",h.value,"hash:",E.value);const o=await y.getACLClients({identity_hash:E.value,identity_name:h.value});console.log("ACL clients response:",o),o.success&&o.data&&(S.value=o.data.clients||[],console.log("ACL clients loaded:",S.value.length))}catch(o){console.error("Failed to fetch ACL clients:",o)}}async function R(o=!1){o&&(I.value=0,w.value=[]),A.value=!0,$.value=null;try{const t=await y.getRoomMessages({room_name:h.value,limit:F.value,offset:I.value});if(t.success&&t.data){const r=t.data.messages||[];o?w.value=r:w.value=[...w.value,...r],O.value=r.length===F.value}else $.value=t.error||"Failed to load messages"}catch(t){$.value=t instanceof Error?t.message:"Failed to load messages"}finally{A.value=!1}}async function ue(){I.value+=F.value,await R(!1)}async function T(){if(_.value.trim())try{const o=await y.postRoomMessage({room_name:h.value,message:_.value,author_pubkey:"server"});o.success?(_.value="",await R(!0)):u(`Failed to send message: ${o.error}`,"error")}catch(o){u(`Error sending message: ${o}`,"error")}}async function ce(o){if(confirm("Are you sure you want to delete this message?"))try{const t=await y.deleteRoomMessage({room_name:h.value,message_id:o});t.success?(await R(!0),u("Message deleted successfully","success")):u(`Failed to delete message: ${t.error}`,"error")}catch(t){u(`Error deleting message: ${t}`,"error")}}function pe(){K.value=!1,h.value="",E.value="",w.value=[],_.value="",$.value=null,S.value=[]}function me(o){return o?new Date(o*1e3).toLocaleString():"Unknown"}async function ve(o,t){if(confirm("Are you sure you want to remove this client from the ACL?"))try{const r=await y.removeACLClient({public_key:o,identity_hash:t});r.success?(await X(),u("Client removed successfully","success")):u(`Failed to remove client: ${r.error}`,"error")}catch(r){u(`Error removing client: ${r}`,"error")}}return(o,t)=>(n(),s(N,null,[e("div",$e,[e("div",Se,[t[26]||(t[26]=e("div",{class:"absolute inset-0 bg-gradient-to-br from-primary/20 via-secondary/10 to-accent-purple/20 opacity-50"},null,-1)),t[27]||(t[27]=e("div",{class:"absolute inset-0 bg-gradient-to-tl from-accent-green/10 via-transparent to-primary/10 animate-pulse"},null,-1)),e("div",Be,[t[25]||(t[25]=G('Manage room server identities and messages
No messages yet
Be the first to start the conversation
Welcome to your pyMC Repeater! Let's get you set up in just a few steps.
You'll configure:
Packet Rate (RX/TX PER HOUR)