mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-07 09:23:06 +02:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -169,3 +169,9 @@ logging:
|
|||||||
|
|
||||||
# Log format
|
# Log format
|
||||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
|
||||||
|
# Web interface configuration
|
||||||
|
web:
|
||||||
|
# Enable Cross-Origin Resource Sharing (CORS) headers
|
||||||
|
# Allows web frontends from different origins to access the API
|
||||||
|
cors_enabled: false
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ class RRDToolHandler:
|
|||||||
|
|
||||||
def update_packet_metrics(self, record: dict, cumulative_counts: dict):
|
def update_packet_metrics(self, record: dict, cumulative_counts: dict):
|
||||||
if not self.available or not self.rrd_path.exists():
|
if not self.available or not self.rrd_path.exists():
|
||||||
logger.debug("RRD not available or doesn't exist for packet metrics")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -83,9 +82,7 @@ class RRDToolHandler:
|
|||||||
try:
|
try:
|
||||||
info = rrdtool.info(str(self.rrd_path))
|
info = rrdtool.info(str(self.rrd_path))
|
||||||
last_update = int(info.get("last_update", timestamp - 60))
|
last_update = int(info.get("last_update", timestamp - 60))
|
||||||
logger.debug(f"RRD packet update: timestamp={timestamp}, last_update={last_update}")
|
|
||||||
if timestamp <= last_update:
|
if timestamp <= last_update:
|
||||||
logger.debug(f"Skipping RRD packet update: timestamp {timestamp} <= last_update {last_update}")
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Failed to get RRD info for packet update: {e}")
|
logger.debug(f"Failed to get RRD info for packet update: {e}")
|
||||||
@@ -108,9 +105,7 @@ class RRDToolHandler:
|
|||||||
type_values_str = ":".join(type_values)
|
type_values_str = ":".join(type_values)
|
||||||
values = f"{basic_values}:{type_values_str}"
|
values = f"{basic_values}:{type_values_str}"
|
||||||
|
|
||||||
logger.debug(f"Updating RRD with packet values: {values}")
|
|
||||||
rrdtool.update(str(self.rrd_path), values)
|
rrdtool.update(str(self.rrd_path), values)
|
||||||
logger.debug(f"RRD packet update successful")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to update RRD packet metrics: {e}")
|
logger.error(f"Failed to update RRD packet metrics: {e}")
|
||||||
@@ -128,8 +123,6 @@ class RRDToolHandler:
|
|||||||
if start_time is None:
|
if start_time is None:
|
||||||
start_time = end_time - (24 * 3600)
|
start_time = end_time - (24 * 3600)
|
||||||
|
|
||||||
logger.debug(f"RRD fetch: start={start_time}, end={end_time}, resolution={resolution}")
|
|
||||||
|
|
||||||
fetch_result = rrdtool.fetch(
|
fetch_result = rrdtool.fetch(
|
||||||
str(self.rrd_path),
|
str(self.rrd_path),
|
||||||
resolution.upper(),
|
resolution.upper(),
|
||||||
@@ -142,13 +135,8 @@ class RRDToolHandler:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
(start, end, step), data_sources, data_points = fetch_result
|
(start, end, step), data_sources, data_points = fetch_result
|
||||||
logger.debug(f"RRD fetch result: start={start}, end={end}, step={step}, sources={len(data_sources)}, points={len(data_points)}")
|
|
||||||
logger.debug(f"Data sources: {data_sources}")
|
|
||||||
|
|
||||||
if data_points:
|
if not data_points:
|
||||||
logger.debug(f"First data point: {data_points[0]}")
|
|
||||||
logger.debug(f"Last data point: {data_points[-1]}")
|
|
||||||
else:
|
|
||||||
logger.warning("No data points returned from RRD fetch")
|
logger.warning("No data points returned from RRD fetch")
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
@@ -184,13 +172,6 @@ class RRDToolHandler:
|
|||||||
current_time += step
|
current_time += step
|
||||||
|
|
||||||
result['timestamps'] = timestamps
|
result['timestamps'] = timestamps
|
||||||
logger.debug(f"RRD data processed successfully: {len(timestamps)} timestamps, packet_types keys: {list(result['packet_types'].keys())}")
|
|
||||||
|
|
||||||
for type_key in ['type_2', 'type_4', 'type_5']:
|
|
||||||
if type_key in result['packet_types']:
|
|
||||||
values = result['packet_types'][type_key]
|
|
||||||
non_none_values = [v for v in values if v is not None]
|
|
||||||
logger.debug(f"{type_key} values: count={len(values)}, non-none={len(non_none_values)}, sample={values[:3] if values else 'empty'}")
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -203,15 +184,11 @@ class RRDToolHandler:
|
|||||||
end_time = int(time.time())
|
end_time = int(time.time())
|
||||||
start_time = end_time - (hours * 3600)
|
start_time = end_time - (hours * 3600)
|
||||||
|
|
||||||
logger.debug(f"Getting packet type stats for {hours} hours from {start_time} to {end_time}")
|
|
||||||
|
|
||||||
rrd_data = self.get_data(start_time, end_time)
|
rrd_data = self.get_data(start_time, end_time)
|
||||||
if not rrd_data or 'packet_types' not in rrd_data:
|
if not rrd_data or 'packet_types' not in rrd_data:
|
||||||
logger.warning(f"No RRD data available")
|
logger.warning(f"No RRD data available")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.debug(f"RRD packet_types keys: {list(rrd_data['packet_types'].keys())}")
|
|
||||||
|
|
||||||
type_totals = {}
|
type_totals = {}
|
||||||
packet_type_names = {
|
packet_type_names = {
|
||||||
'type_0': 'Request (REQ)',
|
'type_0': 'Request (REQ)',
|
||||||
@@ -244,23 +221,17 @@ class RRDToolHandler:
|
|||||||
|
|
||||||
for type_key, data_points in rrd_data['packet_types'].items():
|
for type_key, data_points in rrd_data['packet_types'].items():
|
||||||
valid_points = [p for p in data_points if p is not None]
|
valid_points = [p for p in data_points if p is not None]
|
||||||
logger.debug(f"{type_key}: total_points={len(data_points)}, valid_points={len(valid_points)}")
|
|
||||||
|
|
||||||
if len(valid_points) >= 2:
|
if len(valid_points) >= 2:
|
||||||
total = max(valid_points) - min(valid_points)
|
total = max(valid_points) - min(valid_points)
|
||||||
logger.debug(f"{type_key}: min={min(valid_points)}, max={max(valid_points)}, total={total}")
|
|
||||||
elif len(valid_points) == 1:
|
elif len(valid_points) == 1:
|
||||||
total = valid_points[0]
|
total = valid_points[0]
|
||||||
logger.debug(f"{type_key}: single value={total}")
|
|
||||||
else:
|
else:
|
||||||
total = 0
|
total = 0
|
||||||
logger.debug(f"{type_key}: no valid values, total=0")
|
|
||||||
|
|
||||||
type_name = packet_type_names.get(type_key, type_key)
|
type_name = packet_type_names.get(type_key, type_key)
|
||||||
type_totals[type_name] = max(0, total or 0)
|
type_totals[type_name] = max(0, total or 0)
|
||||||
|
|
||||||
logger.debug(f"Final type_totals: {type_totals}")
|
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"hours": hours,
|
"hours": hours,
|
||||||
"packet_type_totals": type_totals,
|
"packet_type_totals": type_totals,
|
||||||
@@ -269,7 +240,6 @@ class RRDToolHandler:
|
|||||||
"data_source": "rrd"
|
"data_source": "rrd"
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(f"Returning packet type stats: {result}")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+21
-2
@@ -478,6 +478,7 @@ class RepeaterHandler(BaseHandler):
|
|||||||
for key_record in transport_keys:
|
for key_record in transport_keys:
|
||||||
transport_key_encoded = key_record.get("transport_key")
|
transport_key_encoded = key_record.get("transport_key")
|
||||||
key_name = key_record.get("name", "unknown")
|
key_name = key_record.get("name", "unknown")
|
||||||
|
flood_policy = key_record.get("flood_policy", "deny")
|
||||||
|
|
||||||
if not transport_key_encoded:
|
if not transport_key_encoded:
|
||||||
continue
|
continue
|
||||||
@@ -487,8 +488,26 @@ class RepeaterHandler(BaseHandler):
|
|||||||
transport_key = base64.b64decode(transport_key_encoded)
|
transport_key = base64.b64decode(transport_key_encoded)
|
||||||
expected_code = calc_transport_code(transport_key, packet)
|
expected_code = calc_transport_code(transport_key, packet)
|
||||||
if transport_code_0 == expected_code:
|
if transport_code_0 == expected_code:
|
||||||
logger.debug(f"Transport code validated for key '{key_name}'")
|
logger.debug(f"Transport code validated for key '{key_name}' with policy '{flood_policy}'")
|
||||||
return True, ""
|
|
||||||
|
# Update last_used timestamp for this key
|
||||||
|
try:
|
||||||
|
key_id = key_record.get("id")
|
||||||
|
if key_id:
|
||||||
|
self.storage.update_transport_key(
|
||||||
|
key_id=key_id,
|
||||||
|
last_used=time.time()
|
||||||
|
)
|
||||||
|
logger.debug(f"Updated last_used timestamp for transport key '{key_name}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to update last_used for transport key '{key_name}': {e}")
|
||||||
|
|
||||||
|
# Check flood policy for this key
|
||||||
|
if flood_policy == "allow":
|
||||||
|
return True, ""
|
||||||
|
else:
|
||||||
|
return False, f"Transport key '{key_name}' flood policy denied"
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error checking transport key '{key_name}': {e}")
|
logger.warning(f"Error checking transport key '{key_name}': {e}")
|
||||||
|
|||||||
@@ -351,7 +351,6 @@ class RepeaterDaemon:
|
|||||||
http_port = self.config.get("http", {}).get("port", 8000)
|
http_port = self.config.get("http", {}).get("port", 8000)
|
||||||
http_host = self.config.get("http", {}).get("host", "0.0.0.0")
|
http_host = self.config.get("http", {}).get("host", "0.0.0.0")
|
||||||
|
|
||||||
template_dir = os.path.join(os.path.dirname(__file__), "templates")
|
|
||||||
node_name = self.config.get("repeater", {}).get("node_name", "Repeater")
|
node_name = self.config.get("repeater", {}).get("node_name", "Repeater")
|
||||||
|
|
||||||
# Format public key for display
|
# Format public key for display
|
||||||
@@ -370,7 +369,6 @@ class RepeaterDaemon:
|
|||||||
host=http_host,
|
host=http_host,
|
||||||
port=http_port,
|
port=http_port,
|
||||||
stats_getter=self.get_stats,
|
stats_getter=self.get_stats,
|
||||||
template_dir=template_dir,
|
|
||||||
node_name=node_name,
|
node_name=node_name,
|
||||||
pub_key=pub_key_formatted,
|
pub_key=pub_key_formatted,
|
||||||
send_advert_func=self.send_advert,
|
send_advert_func=self.send_advert,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
@@ -11,6 +12,11 @@ from .cad_calibration_engine import CADCalibrationEngine
|
|||||||
logger = logging.getLogger("HTTPServer")
|
logger = logging.getLogger("HTTPServer")
|
||||||
|
|
||||||
|
|
||||||
|
def is_cors_enabled(config: dict) -> bool:
|
||||||
|
"""Check if CORS is enabled in the configuration"""
|
||||||
|
return config.get("web", {}).get("cors_enabled", False)
|
||||||
|
|
||||||
|
|
||||||
def add_cors_headers():
|
def add_cors_headers():
|
||||||
"""Add CORS headers to allow cross-origin requests"""
|
"""Add CORS headers to allow cross-origin requests"""
|
||||||
cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
|
cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
|
||||||
@@ -18,14 +24,6 @@ def add_cors_headers():
|
|||||||
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||||
|
|
||||||
|
|
||||||
def cors_enabled(func):
|
|
||||||
"""Decorator to enable CORS for API endpoints"""
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
add_cors_headers()
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
# system systems
|
# system systems
|
||||||
# GET /api/stats
|
# GET /api/stats
|
||||||
# GET /api/logs
|
# GET /api/logs
|
||||||
@@ -77,12 +75,25 @@ class APIEndpoints:
|
|||||||
self.daemon_instance = daemon_instance
|
self.daemon_instance = daemon_instance
|
||||||
self._config_path = config_path or '/etc/pymc_repeater/config.yaml'
|
self._config_path = config_path or '/etc/pymc_repeater/config.yaml'
|
||||||
self.cad_calibration = CADCalibrationEngine(daemon_instance, event_loop)
|
self.cad_calibration = CADCalibrationEngine(daemon_instance, event_loop)
|
||||||
|
|
||||||
|
# CORS check from config
|
||||||
|
self._cors_enabled = is_cors_enabled(self.config)
|
||||||
|
|
||||||
|
# Log the CORS status
|
||||||
|
logger.info(f"CORS {'enabled' if self._cors_enabled else 'disabled'} (config: web.cors_enabled={self._cors_enabled})")
|
||||||
|
|
||||||
|
# Set up automatic CORS for all responses if enabled
|
||||||
|
if self._cors_enabled:
|
||||||
|
cherrypy.engine.subscribe('before_finalize', self._add_cors_headers)
|
||||||
|
|
||||||
|
def _add_cors_headers(self):
|
||||||
|
"""Automatically add CORS headers to all responses"""
|
||||||
|
add_cors_headers()
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def default(self, *args, **kwargs):
|
def default(self, *args, **kwargs):
|
||||||
"""Handle OPTIONS requests for CORS preflight"""
|
"""Handle OPTIONS requests for CORS preflight"""
|
||||||
if cherrypy.request.method == "OPTIONS":
|
if cherrypy.request.method == "OPTIONS":
|
||||||
add_cors_headers()
|
|
||||||
return ""
|
return ""
|
||||||
# For non-OPTIONS requests, return 404
|
# For non-OPTIONS requests, return 404
|
||||||
raise cherrypy.HTTPError(404)
|
raise cherrypy.HTTPError(404)
|
||||||
@@ -122,7 +133,9 @@ class APIEndpoints:
|
|||||||
|
|
||||||
def _require_post(self):
|
def _require_post(self):
|
||||||
if cherrypy.request.method != "POST":
|
if cherrypy.request.method != "POST":
|
||||||
raise Exception("Method not allowed")
|
cherrypy.response.status = 405 # Method Not Allowed
|
||||||
|
cherrypy.response.headers['Allow'] = 'POST'
|
||||||
|
raise cherrypy.HTTPError(405, "Method not allowed. This endpoint requires POST.")
|
||||||
|
|
||||||
def _get_time_range(self, hours):
|
def _get_time_range(self, hours):
|
||||||
end_time = int(time.time())
|
end_time = int(time.time())
|
||||||
@@ -147,7 +160,6 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def stats(self):
|
def stats(self):
|
||||||
try:
|
try:
|
||||||
stats = self.stats_getter() if self.stats_getter else {}
|
stats = self.stats_getter() if self.stats_getter else {}
|
||||||
@@ -164,7 +176,6 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def send_advert(self):
|
def send_advert(self):
|
||||||
try:
|
try:
|
||||||
self._require_post()
|
self._require_post()
|
||||||
@@ -176,6 +187,9 @@ class APIEndpoints:
|
|||||||
future = asyncio.run_coroutine_threadsafe(self.send_advert_func(), self.event_loop)
|
future = asyncio.run_coroutine_threadsafe(self.send_advert_func(), self.event_loop)
|
||||||
result = future.result(timeout=10)
|
result = future.result(timeout=10)
|
||||||
return self._success("Advert sent successfully") if result else self._error("Failed to send advert")
|
return self._success("Advert sent successfully") if result else self._error("Failed to send advert")
|
||||||
|
except cherrypy.HTTPError:
|
||||||
|
# Re-raise HTTP errors (like 405 Method Not Allowed) without logging
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending advert: {e}", exc_info=True)
|
logger.error(f"Error sending advert: {e}", exc_info=True)
|
||||||
return self._error(e)
|
return self._error(e)
|
||||||
@@ -183,7 +197,6 @@ class APIEndpoints:
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cherrypy.tools.json_in()
|
@cherrypy.tools.json_in()
|
||||||
@cors_enabled
|
|
||||||
def set_mode(self):
|
def set_mode(self):
|
||||||
try:
|
try:
|
||||||
self._require_post()
|
self._require_post()
|
||||||
@@ -196,6 +209,9 @@ class APIEndpoints:
|
|||||||
self.config["repeater"]["mode"] = new_mode
|
self.config["repeater"]["mode"] = new_mode
|
||||||
logger.info(f"Mode changed to: {new_mode}")
|
logger.info(f"Mode changed to: {new_mode}")
|
||||||
return {"success": True, "mode": new_mode}
|
return {"success": True, "mode": new_mode}
|
||||||
|
except cherrypy.HTTPError:
|
||||||
|
# Re-raise HTTP errors (like 405 Method Not Allowed) without logging
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting mode: {e}", exc_info=True)
|
logger.error(f"Error setting mode: {e}", exc_info=True)
|
||||||
return self._error(e)
|
return self._error(e)
|
||||||
@@ -203,7 +219,6 @@ class APIEndpoints:
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cherrypy.tools.json_in()
|
@cherrypy.tools.json_in()
|
||||||
@cors_enabled
|
|
||||||
def set_duty_cycle(self):
|
def set_duty_cycle(self):
|
||||||
try:
|
try:
|
||||||
self._require_post()
|
self._require_post()
|
||||||
@@ -214,13 +229,15 @@ class APIEndpoints:
|
|||||||
self.config["duty_cycle"]["enforcement_enabled"] = enabled
|
self.config["duty_cycle"]["enforcement_enabled"] = enabled
|
||||||
logger.info(f"Duty cycle enforcement {'enabled' if enabled else 'disabled'}")
|
logger.info(f"Duty cycle enforcement {'enabled' if enabled else 'disabled'}")
|
||||||
return {"success": True, "enabled": enabled}
|
return {"success": True, "enabled": enabled}
|
||||||
|
except cherrypy.HTTPError:
|
||||||
|
# Re-raise HTTP errors (like 405 Method Not Allowed) without logging
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting duty cycle: {e}", exc_info=True)
|
logger.error(f"Error setting duty cycle: {e}", exc_info=True)
|
||||||
return self._error(e)
|
return self._error(e)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def logs(self):
|
def logs(self):
|
||||||
from .http_server import _log_buffer
|
from .http_server import _log_buffer
|
||||||
try:
|
try:
|
||||||
@@ -244,8 +261,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def packet_stats(self, hours=24):
|
def packet_stats(self, hours=24):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
stats = self._get_storage().get_packet_stats(hours=hours)
|
stats = self._get_storage().get_packet_stats(hours=hours)
|
||||||
@@ -256,8 +273,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def packet_type_stats(self, hours=24):
|
def packet_type_stats(self, hours=24):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
stats = self._get_storage().get_packet_type_stats(hours=hours)
|
stats = self._get_storage().get_packet_type_stats(hours=hours)
|
||||||
@@ -268,8 +285,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def route_stats(self, hours=24):
|
def route_stats(self, hours=24):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
stats = self._get_storage().get_route_stats(hours=hours)
|
stats = self._get_storage().get_route_stats(hours=hours)
|
||||||
@@ -280,8 +297,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def recent_packets(self, limit=100):
|
def recent_packets(self, limit=100):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
limit = int(limit)
|
limit = int(limit)
|
||||||
packets = self._get_storage().get_recent_packets(limit=limit)
|
packets = self._get_storage().get_recent_packets(limit=limit)
|
||||||
@@ -351,8 +368,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def packet_type_graph_data(self, hours=24, resolution='average', types='all'):
|
def packet_type_graph_data(self, hours=24, resolution='average', types='all'):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
start_time, end_time = self._get_time_range(hours)
|
start_time, end_time = self._get_time_range(hours)
|
||||||
@@ -398,8 +415,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def metrics_graph_data(self, hours=24, resolution='average', metrics='all'):
|
def metrics_graph_data(self, hours=24, resolution='average', metrics='all'):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
start_time, end_time = self._get_time_range(hours)
|
start_time, end_time = self._get_time_range(hours)
|
||||||
@@ -460,8 +477,8 @@ class APIEndpoints:
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cherrypy.tools.json_in()
|
@cherrypy.tools.json_in()
|
||||||
@cors_enabled
|
|
||||||
def cad_calibration_start(self):
|
def cad_calibration_start(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._require_post()
|
self._require_post()
|
||||||
data = cherrypy.request.json or {}
|
data = cherrypy.request.json or {}
|
||||||
@@ -471,18 +488,24 @@ class APIEndpoints:
|
|||||||
return self._success("Calibration started")
|
return self._success("Calibration started")
|
||||||
else:
|
else:
|
||||||
return self._error("Calibration already running")
|
return self._error("Calibration already running")
|
||||||
|
except cherrypy.HTTPError:
|
||||||
|
# Re-raise HTTP errors (like 405 Method Not Allowed) without logging
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error starting CAD calibration: {e}")
|
logger.error(f"Error starting CAD calibration: {e}")
|
||||||
return self._error(e)
|
return self._error(e)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def cad_calibration_stop(self):
|
def cad_calibration_stop(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._require_post()
|
self._require_post()
|
||||||
self.cad_calibration.stop_calibration()
|
self.cad_calibration.stop_calibration()
|
||||||
return self._success("Calibration stopped")
|
return self._success("Calibration stopped")
|
||||||
|
except cherrypy.HTTPError:
|
||||||
|
# Re-raise HTTP errors (like 405 Method Not Allowed) without logging
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error stopping CAD calibration: {e}")
|
logger.error(f"Error stopping CAD calibration: {e}")
|
||||||
return self._error(e)
|
return self._error(e)
|
||||||
@@ -490,8 +513,8 @@ class APIEndpoints:
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cherrypy.tools.json_in()
|
@cherrypy.tools.json_in()
|
||||||
@cors_enabled
|
|
||||||
def save_cad_settings(self):
|
def save_cad_settings(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._require_post()
|
self._require_post()
|
||||||
data = cherrypy.request.json or {}
|
data = cherrypy.request.json or {}
|
||||||
@@ -524,6 +547,9 @@ class APIEndpoints:
|
|||||||
"message": f"CAD settings saved: peak={peak}, min={min_val}",
|
"message": f"CAD settings saved: peak={peak}, min={min_val}",
|
||||||
"settings": {"peak": peak, "min_val": min_val, "detection_rate": detection_rate}
|
"settings": {"peak": peak, "min_val": min_val, "detection_rate": detection_rate}
|
||||||
}
|
}
|
||||||
|
except cherrypy.HTTPError:
|
||||||
|
# Re-raise HTTP errors (like 405 Method Not Allowed) without logging
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving CAD settings: {e}")
|
logger.error(f"Error saving CAD settings: {e}")
|
||||||
return self._error(e)
|
return self._error(e)
|
||||||
@@ -542,8 +568,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def noise_floor_history(self, hours: int = 24):
|
def noise_floor_history(self, hours: int = 24):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
storage = self._get_storage()
|
storage = self._get_storage()
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
@@ -560,8 +586,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def noise_floor_stats(self, hours: int = 24):
|
def noise_floor_stats(self, hours: int = 24):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
storage = self._get_storage()
|
storage = self._get_storage()
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
@@ -577,8 +603,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def noise_floor_chart_data(self, hours: int = 24):
|
def noise_floor_chart_data(self, hours: int = 24):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
storage = self._get_storage()
|
storage = self._get_storage()
|
||||||
hours = int(hours)
|
hours = int(hours)
|
||||||
@@ -597,7 +623,10 @@ class APIEndpoints:
|
|||||||
cherrypy.response.headers['Content-Type'] = 'text/event-stream'
|
cherrypy.response.headers['Content-Type'] = 'text/event-stream'
|
||||||
cherrypy.response.headers['Cache-Control'] = 'no-cache'
|
cherrypy.response.headers['Cache-Control'] = 'no-cache'
|
||||||
cherrypy.response.headers['Connection'] = 'keep-alive'
|
cherrypy.response.headers['Connection'] = 'keep-alive'
|
||||||
cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
|
|
||||||
|
# Add CORS headers conditionally for SSE endpoint
|
||||||
|
if self._cors_enabled:
|
||||||
|
cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
|
||||||
if not hasattr(self.cad_calibration, 'message_queue'):
|
if not hasattr(self.cad_calibration, 'message_queue'):
|
||||||
self.cad_calibration.message_queue = []
|
self.cad_calibration.message_queue = []
|
||||||
@@ -651,9 +680,8 @@ class APIEndpoints:
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cors_enabled
|
|
||||||
def adverts_by_contact_type(self, contact_type=None, limit=None, hours=None):
|
def adverts_by_contact_type(self, contact_type=None, limit=None, hours=None):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not contact_type:
|
if not contact_type:
|
||||||
return self._error("contact_type parameter is required")
|
return self._error("contact_type parameter is required")
|
||||||
@@ -686,8 +714,8 @@ class APIEndpoints:
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cherrypy.tools.json_in()
|
@cherrypy.tools.json_in()
|
||||||
@cors_enabled
|
|
||||||
def transport_keys(self):
|
def transport_keys(self):
|
||||||
|
|
||||||
if cherrypy.request.method == "GET":
|
if cherrypy.request.method == "GET":
|
||||||
try:
|
try:
|
||||||
storage = self._get_storage()
|
storage = self._get_storage()
|
||||||
@@ -738,8 +766,8 @@ class APIEndpoints:
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cherrypy.tools.json_in()
|
@cherrypy.tools.json_in()
|
||||||
@cors_enabled
|
|
||||||
def transport_key(self, key_id):
|
def transport_key(self, key_id):
|
||||||
|
|
||||||
if cherrypy.request.method == "GET":
|
if cherrypy.request.method == "GET":
|
||||||
try:
|
try:
|
||||||
key_id = int(key_id)
|
key_id = int(key_id)
|
||||||
@@ -810,8 +838,8 @@ class APIEndpoints:
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@cherrypy.tools.json_in()
|
@cherrypy.tools.json_in()
|
||||||
@cors_enabled
|
|
||||||
def global_flood_policy(self):
|
def global_flood_policy(self):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Update global flood policy configuration
|
Update global flood policy configuration
|
||||||
|
|
||||||
|
|||||||
+11
-11
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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-BJnLrjMq.js"></script>
|
<script type="module" crossorigin src="/assets/index-DJUYEmmJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-wgCsqUA2.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-wgCsqUA2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+46
-14
@@ -46,7 +46,6 @@ class StatsApp:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
stats_getter: Optional[Callable] = None,
|
stats_getter: Optional[Callable] = None,
|
||||||
template_dir: Optional[str] = None,
|
|
||||||
node_name: str = "Repeater",
|
node_name: str = "Repeater",
|
||||||
pub_key: str = "",
|
pub_key: str = "",
|
||||||
send_advert_func: Optional[Callable] = None,
|
send_advert_func: Optional[Callable] = None,
|
||||||
@@ -57,20 +56,39 @@ class StatsApp:
|
|||||||
):
|
):
|
||||||
|
|
||||||
self.stats_getter = stats_getter
|
self.stats_getter = stats_getter
|
||||||
self.template_dir = template_dir
|
|
||||||
self.node_name = node_name
|
self.node_name = node_name
|
||||||
self.pub_key = pub_key
|
self.pub_key = pub_key
|
||||||
self.dashboard_template = None
|
self.dashboard_template = None
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
|
|
||||||
|
# Path to the compiled Vue.js application
|
||||||
|
self.html_dir = os.path.join(os.path.dirname(__file__), "html")
|
||||||
|
|
||||||
# Create nested API object for routing
|
# Create nested API object for routing
|
||||||
self.api = APIEndpoints(stats_getter, send_advert_func, self.config, event_loop, daemon_instance, config_path)
|
self.api = APIEndpoints(stats_getter, send_advert_func, self.config, event_loop, daemon_instance, config_path)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
"""Serve the Vue.js application index.html."""
|
||||||
|
index_path = os.path.join(self.html_dir, "index.html")
|
||||||
|
try:
|
||||||
|
with open(index_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise cherrypy.HTTPError(404, "Application not found. Please build the frontend first.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error serving index.html: {e}")
|
||||||
|
raise cherrypy.HTTPError(500, "Internal server error")
|
||||||
|
|
||||||
# @cherrypy.expose
|
@cherrypy.expose
|
||||||
# def index(self):
|
def default(self, *args, **kwargs):
|
||||||
# """Serve dashboard HTML."""
|
"""Handle client-side routing - serve index.html for all non-API routes."""
|
||||||
# return self._serve_template("dashboard.html")
|
# Let API routes pass through
|
||||||
|
if args and args[0] == 'api':
|
||||||
|
raise cherrypy.NotFound()
|
||||||
|
|
||||||
|
# For all other routes, serve the Vue.js app (client-side routing)
|
||||||
|
return self.index()
|
||||||
|
|
||||||
|
|
||||||
class HTTPStatsServer:
|
class HTTPStatsServer:
|
||||||
@@ -80,7 +98,6 @@ class HTTPStatsServer:
|
|||||||
host: str = "0.0.0.0",
|
host: str = "0.0.0.0",
|
||||||
port: int = 8000,
|
port: int = 8000,
|
||||||
stats_getter: Optional[Callable] = None,
|
stats_getter: Optional[Callable] = None,
|
||||||
template_dir: Optional[str] = None,
|
|
||||||
node_name: str = "Repeater",
|
node_name: str = "Repeater",
|
||||||
pub_key: str = "",
|
pub_key: str = "",
|
||||||
send_advert_func: Optional[Callable] = None,
|
send_advert_func: Optional[Callable] = None,
|
||||||
@@ -93,24 +110,39 @@ class HTTPStatsServer:
|
|||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.app = StatsApp(
|
self.app = StatsApp(
|
||||||
stats_getter, template_dir, node_name, pub_key, send_advert_func, config, event_loop, daemon_instance, config_path
|
stats_getter, node_name, pub_key, send_advert_func, config, event_loop, daemon_instance, config_path
|
||||||
)
|
)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Serve static files from templates directory
|
# Serve static files from the html directory (compiled Vue.js app)
|
||||||
static_dir = (
|
html_dir = os.path.join(os.path.dirname(__file__), "html")
|
||||||
self.app.template_dir if self.app.template_dir else os.path.dirname(__file__)
|
assets_dir = os.path.join(html_dir, "assets")
|
||||||
)
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"/": {
|
"/": {
|
||||||
"tools.sessions.on": False,
|
"tools.sessions.on": False,
|
||||||
|
# Ensure proper content types for Vue.js files
|
||||||
|
"tools.staticfile.content_types": {
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'css': 'text/css',
|
||||||
|
'html': 'text/html; charset=utf-8'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"/static": {
|
"/assets": {
|
||||||
"tools.staticdir.on": True,
|
"tools.staticdir.on": True,
|
||||||
"tools.staticdir.dir": static_dir,
|
"tools.staticdir.dir": assets_dir,
|
||||||
|
# Set proper content types for assets
|
||||||
|
"tools.staticdir.content_types": {
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'css': 'text/css',
|
||||||
|
'map': 'application/json'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/favicon.ico": {
|
||||||
|
"tools.staticfile.on": True,
|
||||||
|
"tools.staticfile.filename": os.path.join(html_dir, "favicon.ico"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user