Refactor code structure for improved readability and maintainability

This commit is contained in:
Lloyd
2025-11-22 22:07:46 +00:00
parent 2495f08ab9
commit 2137e6a1c6
8 changed files with 147 additions and 94 deletions
+6
View File
@@ -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
+1 -31
View File
@@ -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
View File
@@ -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}")
-2
View File
@@ -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,
+61 -33
View File
@@ -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
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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
View File
@@ -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"),
}, },
} }