makeup
This commit is contained in:
@@ -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,19 +208,19 @@ 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:
|
||||
@@ -229,9 +229,9 @@ def on_message(client, userdata, msg):
|
||||
# -----------------------------------------
|
||||
|
||||
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/<int:user_id>', 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/<int:user_id>', 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/<cid>/<service>', 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')
|
||||
|
||||
Reference in New Issue
Block a user