From 69d7e885bd54c6b57864bed173b6813c25f9835d Mon Sep 17 00:00:00 2001 From: Roby Date: Wed, 22 Apr 2026 02:58:07 +0200 Subject: [PATCH] makeup --- app.py | 117 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/app.py b/app.py index 6dc01a5..e62eb11 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,7 @@ from pywebpush import webpush, WebPushException from logging.handlers import RotatingFileHandler from flask_socketio import SocketIO, emit -# --- CONFIGURAZIONE LOGGING --- +# --- LOGGING CONFIGURATION --- logging.basicConfig( handlers=[ RotatingFileHandler('/opt/web-control/fleet_console.log', maxBytes=10000000, backupCount=3), @@ -23,10 +23,10 @@ logging.basicConfig( datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger("FleetHub") -# Silenzia lo spam delle richieste HTTP (GET /api/states 200 OK) +# Silence HTTP request spam (GET /api/states 200 OK) logging.getLogger('werkzeug').setLevel(logging.ERROR) -# --- PERCORSI --- +# --- PATHS --- DB_PATH = '/opt/web-control/monitor.db' CACHE_FILE = '/opt/web-control/telemetry_cache.json' CONFIG_PATH = '/opt/web-control/config.json' @@ -38,7 +38,7 @@ def init_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() - c.execute('PRAGMA journal_mode=WAL;') # <-- MAGIA: Abilita letture/scritture simultanee! + c.execute('PRAGMA journal_mode=WAL;') # <-- MAGIC: Enable simultaneous read/write! c.execute('''CREATE TABLE IF NOT EXISTS radio_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME, client_id TEXT, @@ -63,14 +63,14 @@ def init_db(): h = generate_password_hash('admin123') c.execute("INSERT INTO users (username, password_hash, role, allowed_nodes) VALUES (?,?,?,?)", ('admin', h, 'admin', 'all')) - logger.info(">>> UTENTE DI DEFAULT CREATO - User: admin | Pass: admin123 <<<") + logger.info(">>> DEFAULT USER CREATED - User: admin | Pass: admin123 <<<") conn.commit() conn.close() init_db() -# --- CARICAMENTO DATABASE ID --- +# --- ID DATABASE LOADING --- user_db = {} nxdn_db = {} @@ -134,10 +134,10 @@ if os.path.exists(CACHE_FILE): active_calls = {} with open(CONFIG_PATH) as f: config = json.load(f) -# --- CALLBACKS MQTT --- +# --- MQTT CALLBACKS --- def on_connect(client, userdata, flags, reason_code, properties=None): if reason_code == 0: - logger.info("✅ Connesso al Broker MQTT con successo! Sottoscrizione ai topic in corso...") + logger.info("✅ Successfully connected to MQTT Broker! Subscribing to topics...") client.subscribe([ ("servizi/+/stat", 0), ("dmr-gateway/+/json", 0), @@ -151,10 +151,10 @@ def on_connect(client, userdata, flags, reason_code, properties=None): ("data/#", 0) ]) else: - logger.error(f"❌ Errore di connessione MQTT. Codice motivo: {reason_code}") + logger.error(f"❌ MQTT Connection Error. Reason code: {reason_code}") def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None): - logger.warning(f"⚠️ Disconnessione MQTT rilevata! Codice motivo: {reason_code}. Tentativo di riconnessione automatico in corso...") + logger.warning(f"⚠️ MQTT Disconnection detected! Reason code: {reason_code}. Attempting automatic reconnection...") def on_message(client, userdata, msg): try: @@ -164,7 +164,7 @@ def on_message(client, userdata, msg): if len(parts) < 2: return cid = parts[1].lower() - # --- CATTURA CONFIGURAZIONI COMPLETE --- + # --- CAPTURE FULL CONFIGURATIONS --- if parts[0] == 'data' and len(parts) >= 4 and parts[3] == 'full_config': cid_conf = parts[1].lower() svc_name = parts[2].lower() @@ -172,16 +172,16 @@ def on_message(client, userdata, msg): device_configs[cid_conf] = {} try: device_configs[cid_conf][svc_name] = json.loads(payload) - logger.debug(f"Configurazione salvata per {cid_conf} -> {svc_name}") + logger.debug(f"Configuration saved for {cid_conf} -> {svc_name}") except Exception as e: - logger.error(f"Errore parsing config JSON: {e}") + logger.error(f"Error parsing config JSON: {e}") - # --- GESTIONE STATI SERVIZIO E NODO --- + # --- NODE AND SERVICE STATE MANAGEMENT --- elif parts[0] == 'servizi': client_states[cid] = payload socketio.emit('dati_aggiornati') # <--- WEBSOCKET - # --- GRILLETTO PUSH: STATO NODO --- + # --- PUSH TRIGGER: NODE STATE --- if payload.upper() == 'OFFLINE': if last_notified_errors.get(f"{cid}_NODE") != 'OFFLINE': broadcast_push_notification(f"💀 NODE OFFLINE: {cid.upper()}", "Connection lost with broker.") @@ -194,10 +194,10 @@ def on_message(client, userdata, msg): if payload.upper() not in ['OFF', 'OFFLINE', '']: tel = client_telemetry.get(cid, {}) if isinstance(tel, dict) and '🔄' in str(tel.get('ts1', '')): - client_telemetry[cid] = {"ts1": "In attesa...", "ts2": "In attesa...", "alt": ""} + client_telemetry[cid] = {"ts1": "Waiting...", "ts2": "Waiting...", "alt": ""} save_cache(client_telemetry) - # --- GESTIONE SALUTE DISPOSITIVI --- + # --- DEVICE HEALTH MANAGEMENT --- elif parts[0] == 'devices' and len(parts) >= 3 and parts[2] == 'services': try: data = json.loads(payload) @@ -208,30 +208,30 @@ def on_message(client, userdata, msg): "disk": round(data.get("disk_usage_percent", 0), 1), "processes": data.get("processes", {}), "files": data.get("files", data.get("config_files", [])), - "profiles": data.get("profiles", {"A": "PROFILO A", "B": "PROFILO B"}) + "profiles": data.get("profiles", {"A": "PROFILE A", "B": "PROFILE B"}) } socketio.emit('dati_aggiornati') # <--- WEBSOCKET - # --- GRILLETTO PUSH: SERVIZI IN ERRORE --- + # --- PUSH TRIGGER: SERVICE ERRORS --- processes = data.get("processes", {}) for svc_name, svc_status in processes.items(): status_key = f"{cid}_{svc_name}" s_lower = svc_status.lower() if s_lower in ["error", "stopped", "failed"]: if last_notified_errors.get(status_key) != s_lower: - msg_err = f"Servizio {svc_name} KO ({svc_status})" - if s_lower == "error": msg_err += " - Auto-healing fallito! ⚠️" + msg_err = f"Service {svc_name} KO ({svc_status})" + if s_lower == "error": msg_err += " - Auto-healing failed! ⚠️" broadcast_push_notification(f"🚨 ALARM: {cid.upper()}", msg_err) last_notified_errors[status_key] = s_lower elif s_lower == "online" and status_key in last_notified_errors: - broadcast_push_notification(f"✅ RESTORED: {cid.upper()}", f"Service{svc_name} back ONLINE.") + broadcast_push_notification(f"✅ RESTORED: {cid.upper()}", f"Service {svc_name} back ONLINE.") del last_notified_errors[status_key] # ----------------------------------------- except Exception as e: - logger.error(f"Errore parsing health: {e}") + logger.error(f"Error parsing health data: {e}") - # --- GESTIONE DMR GATEWAY --- + # --- DMR GATEWAY MANAGEMENT --- elif len(parts) >= 4 and parts[0] == 'data' and parts[2].lower() == 'dmrgateway' and (parts[3].upper().startswith('NETWORK') or parts[3].upper().startswith('DMR NETWORK')): try: cid = parts[1].lower() @@ -256,9 +256,9 @@ def on_message(client, userdata, msg): socketio.emit('dati_aggiornati') # <--- WEBSOCKET except Exception as e: - logger.error(f"Errore parsing DMRGateway per {cid}: {e}") + logger.error(f"Error parsing DMRGateway for {cid}: {e}") - # --- GESTIONE ALTRI GATEWAY --- + # --- OTHER GATEWAYS MANAGEMENT --- elif parts[0] in ['dmr-gateway', 'nxdn-gateway', 'ysf-gateway', 'p25-gateway', 'dstar-gateway']: data = json.loads(payload) proto = "DMR" @@ -280,12 +280,12 @@ def on_message(client, userdata, msg): if m: save_to_sqlite(cid, {'source_id': "🌐 " + m, 'destination_id': 'NET'}, protocol=proto) - # --- GESTIONE MMDVM E TRAFFICO --- + # --- MMDVM AND TRAFFIC MANAGEMENT --- elif parts[0] == 'mmdvm': data = json.loads(payload) if cid not in active_calls: active_calls[cid] = {} if cid not in client_telemetry or not isinstance(client_telemetry.get(cid), dict): - client_telemetry[cid] = {"ts1": "In attesa...", "ts2": "In attesa...", "alt": "", "idle": True} + client_telemetry[cid] = {"ts1": "Waiting...", "ts2": "Waiting...", "alt": "", "idle": True} if 'MMDVM' in data and data['MMDVM'].get('mode') == 'idle': client_telemetry[cid]["idle"] = True @@ -344,9 +344,9 @@ def on_message(client, userdata, msg): save_cache(client_telemetry) if k in active_calls[cid]: del active_calls[cid][k] except Exception as e: - logger.error(f"ERRORE MQTT MSG: {e}") + logger.error(f"MQTT MSG ERROR: {e}") -# --- INIZIALIZZAZIONE CLIENT MQTT --- +# --- MQTT CLIENT INITIALIZATION --- mqtt_backend = mqtt_client.Client(mqtt_client.CallbackAPIVersion.VERSION2, "flask_backend") mqtt_backend.username_pw_set(config['mqtt']['user'], config['mqtt']['password']) mqtt_backend.on_connect = on_connect @@ -384,7 +384,7 @@ def get_states(): @app.route('/api/service_control', methods=['POST']) def service_control(): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 d = request.json cid = d.get('clientId').lower() action = d.get('action') @@ -418,7 +418,7 @@ def login(): @app.route('/api/command', methods=['POST']) def cmd(): - if not session.get('logged_in'): return jsonify({"success": False, "error": "Non autenticato"}), 403 + if not session.get('logged_in'): return jsonify({"success": False, "error": "Not authenticated"}), 403 d = request.json cid = d['clientId'].lower() cmd_type = d['type'] @@ -427,7 +427,7 @@ def cmd(): allowed = session.get('allowed_nodes', '') is_allowed = (role == 'admin' or allowed == 'all' or cid in [x.strip() for x in allowed.split(',')]) if cmd_type == 'REBOOT' and role != 'admin': - return jsonify({"success": False, "error": "Solo gli Admin possono riavviare."}), 403 + return jsonify({"success": False, "error": "Only Admins can reboot."}), 403 if is_allowed: mqtt_backend.publish(f"servizi/{cid}/cmnd", cmd_type) conn = sqlite3.connect(DB_PATH) @@ -436,20 +436,19 @@ def cmd(): (username, cid, cmd_type)) conn.commit() conn.close() - client_telemetry[cid] = {"ts1": "🔄 Inviato...", "ts2": "🔄 Inviato...", "alt": ""} + client_telemetry[cid] = {"ts1": "🔄 Sent...", "ts2": "🔄 Sent...", "alt": ""} return jsonify({"success": True}) - return jsonify({"success": False, "error": "Non hai i permessi per questo nodo."}), 403 + return jsonify({"success": False, "error": "You do not have permission for this node."}), 403 -# --- API PER IL PULSANTE DI AGGIORNAMENTO --- @app.route('/api/update_nodes', methods=['POST']) def update_nodes(): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 mqtt_backend.publish("devices/control/request", "update") return jsonify({"success": True}) @app.route('/api/users', methods=['GET']) def get_users(): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row c = conn.cursor() @@ -460,14 +459,14 @@ def get_users(): @app.route('/api/users', methods=['POST']) def add_user(): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 d = request.json username = d.get('username') password = d.get('password') role = d.get('role', 'operator') allowed = d.get('allowed_nodes', '') if not username or not password: - return jsonify({"success": False, "error": "Dati mancanti"}) + return jsonify({"success": False, "error": "Missing data"}) h = generate_password_hash(password) try: conn = sqlite3.connect(DB_PATH) @@ -478,18 +477,18 @@ def add_user(): conn.close() return jsonify({"success": True}) except sqlite3.IntegrityError: - return jsonify({"success": False, "error": "Username già esistente"}) + return jsonify({"success": False, "error": "Username already exists"}) @app.route('/api/users/', methods=['DELETE']) def delete_user(user_id): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT username FROM users WHERE id = ?", (user_id,)) u = c.fetchone() if u and u[0] == session.get('user'): conn.close() - return jsonify({"success": False, "error": "Non puoi cancellare te stesso!"}) + return jsonify({"success": False, "error": "You cannot delete yourself!"}) c.execute("DELETE FROM users WHERE id = ?", (user_id,)) conn.commit() conn.close() @@ -498,7 +497,7 @@ def delete_user(user_id): @app.route('/api/users/', methods=['PUT']) def update_user(user_id): if session.get('role') != 'admin': - return jsonify({"error": "Non autorizzato"}), 403 + return jsonify({"error": "Unauthorized"}), 403 data = request.json role = data.get('role', 'operator') @@ -526,14 +525,14 @@ def update_user(user_id): @app.route('/api/change_password', methods=['POST']) def change_password(): if not session.get('logged_in'): - return jsonify({"success": False, "error": "Non autenticato"}), 403 + return jsonify({"success": False, "error": "Not authenticated"}), 403 d = request.json new_pass = d.get('new_password') user_to_change = d.get('username') if session.get('role') != 'admin' and session.get('user') != user_to_change: - return jsonify({"success": False, "error": "Non autorizzato"}), 403 + return jsonify({"success": False, "error": "Unauthorized"}), 403 if not new_pass: - return jsonify({"success": False, "error": "La password non può essere vuota"}), 400 + return jsonify({"success": False, "error": "Password cannot be empty"}), 400 h = generate_password_hash(new_pass) conn = sqlite3.connect(DB_PATH) c = conn.cursor() @@ -545,7 +544,7 @@ def change_password(): @app.route('/api/global_command', methods=['POST']) def global_cmd(): if session.get('role') != 'admin': - return jsonify({"success": False, "error": "Azione riservata all'Admin!"}), 403 + return jsonify({"success": False, "error": "Admin action only!"}), 403 d = request.json cmd_type = d.get('type') clients_list = [] @@ -575,14 +574,14 @@ def auto_update_ids(): }) now = time.strftime("%H:%M") if now == target_time: - logger.info(f">>> [AUTO-UPDATE] Orario raggiunto ({now}). Download in corso...") + logger.info(f">>> [AUTO-UPDATE] Scheduled time reached ({now}). Downloading...") urllib.request.urlretrieve(urls["dmr"], DMR_IDS_PATH) urllib.request.urlretrieve(urls["nxdn"], NXDN_IDS_PATH) load_ids() - logger.info(f">>> [AUTO-UPDATE] Completato con successo.") + logger.info(f">>> [AUTO-UPDATE] Completed successfully.") time.sleep(65) except Exception as e: - logger.error(f">>> [AUTO-UPDATE] Errore: {e}") + logger.error(f">>> [AUTO-UPDATE] Error: {e}") time.sleep(30) @app.route('/api/ui_config', methods=['GET']) @@ -591,9 +590,9 @@ def get_ui_config(): with open(CONFIG_PATH, 'r') as f: cfg = json.load(f) ui_cfg = cfg.get("ui", { - "profileA_Name": "PROFILO A", + "profileA_Name": "PROFILE A", "profileA_Color": "#3498db", - "profileB_Name": "PROFILO B", + "profileB_Name": "PROFILE B", "profileB_Color": "#9b59b6" }) return jsonify(ui_cfg) @@ -602,7 +601,7 @@ def get_ui_config(): @app.route('/api/config', methods=['GET']) def get_config_api(): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 with open(CONFIG_PATH, 'r') as f: cfg = json.load(f) return jsonify({ @@ -613,7 +612,7 @@ def get_config_api(): @app.route('/api/config', methods=['POST']) def save_config_api(): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 new_data = request.json with open(CONFIG_PATH, 'r') as f: cfg = json.load(f) @@ -627,18 +626,18 @@ def save_config_api(): @app.route('/api/config_file//', methods=['GET']) def get_config_file(cid, service): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 cid = cid.lower() service = service.lower() config_data = device_configs.get(cid, {}).get(service) if not config_data: - return jsonify({"error": "Configurazione non ancora ricevuta. Attendi o invia un comando UPDATE."}), 404 + return jsonify({"error": "Configuration not received yet. Wait or send an UPDATE command."}), 404 return jsonify({"success": True, "data": config_data}) @app.route('/api/config_file', methods=['POST']) def save_config_file(): - if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403 + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 d = request.json cid = d.get('clientId').lower() service = d.get('service').lower() @@ -687,7 +686,7 @@ def broadcast_push_notification(title, body): c.execute("DELETE FROM push_subscriptions WHERE id = ?", (sub_id,)) conn.commit() except Exception as e: - logger.error(f"Errore generico Push: {e}") + logger.error(f"Generic Push Error: {e}") conn.close() @app.route('/api/vapid_public_key')