Files
fleet-control-console/app.py
T
2026-04-22 02:08:51 +02:00

712 lines
30 KiB
Python

from flask import Flask, render_template, request, session, jsonify, send_from_directory
from paho.mqtt import client as mqtt_client
from werkzeug.security import generate_password_hash, check_password_hash
import json
import os
import sqlite3
import urllib.request
import threading
import time
import logging
from pywebpush import webpush, WebPushException
from logging.handlers import RotatingFileHandler
from flask_socketio import SocketIO, emit
# --- CONFIGURAZIONE LOGGING ---
logging.basicConfig(
handlers=[
RotatingFileHandler('/opt/web-control/fleet_console.log', maxBytes=10000000, backupCount=3),
logging.StreamHandler()
],
level=logging.INFO,
format='[%(asctime)s] %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger("FleetHub")
# Silenzia lo spam delle richieste HTTP (GET /api/states 200 OK)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
# --- PERCORSI ---
DB_PATH = '/opt/web-control/monitor.db'
CACHE_FILE = '/opt/web-control/telemetry_cache.json'
CONFIG_PATH = '/opt/web-control/config.json'
DMR_IDS_PATH = '/opt/web-control/dmrid.dat'
NXDN_IDS_PATH = '/opt/web-control/nxdn.csv'
CLIENTS_PATH = '/opt/web-control/clients.json'
def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('PRAGMA journal_mode=WAL;') # <-- MAGIA: Abilita letture/scritture simultanee!
c.execute('''CREATE TABLE IF NOT EXISTS radio_logs
(id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME, client_id TEXT,
source_id TEXT, target TEXT, slot INTEGER, duration REAL, ber REAL, loss REAL)''')
try:
c.execute("ALTER TABLE radio_logs ADD COLUMN protocol TEXT DEFAULT 'DMR'")
except: pass
c.execute('''CREATE TABLE IF NOT EXISTS users
(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT,
role TEXT, allowed_nodes TEXT)''')
c.execute('''CREATE TABLE IF NOT EXISTS push_subscriptions
(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, subscription TEXT UNIQUE)''')
c.execute('''CREATE TABLE IF NOT EXISTS audit_logs
(id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME, username TEXT,
client_id TEXT, command TEXT)''')
c.execute("SELECT COUNT(*) FROM users")
if c.fetchone()[0] == 0:
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 <<<")
conn.commit()
conn.close()
init_db()
# --- CARICAMENTO DATABASE ID ---
user_db = {}
nxdn_db = {}
def load_ids():
global user_db, nxdn_db
user_db.clear()
nxdn_db.clear()
if os.path.exists(DMR_IDS_PATH):
with open(DMR_IDS_PATH, 'r', encoding='utf-8', errors='ignore') as f:
for l in f:
sep = '\t' if '\t' in l else (',' if ',' in l else ';')
p = l.strip().split(sep)
if len(p) >= 2 and p[0].strip().isdigit():
user_db[p[0].strip()] = p[1].strip()
if os.path.exists(NXDN_IDS_PATH):
with open(NXDN_IDS_PATH, 'r', encoding='utf-8', errors='ignore') as f:
for l in f:
sep = '\t' if '\t' in l else (',' if ',' in l else ';')
p = l.strip().split(sep)
if len(p) >= 2 and p[0].strip().isdigit():
nxdn_db[p[0].strip()] = p[1].strip()
load_ids()
def get_call(id, proto="DMR"):
sid = str(id)
if proto == "NXDN": return nxdn_db.get(sid, sid)
return user_db.get(sid, sid)
def save_cache(data):
with open(CACHE_FILE, 'w') as f: json.dump(data, f)
socketio.emit('dati_aggiornati')
def save_to_sqlite(client_id, data, protocol="DMR"):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT INTO radio_logs (timestamp, client_id, protocol, source_id, target, slot, duration, ber) VALUES (datetime('now', 'localtime'), ?, ?, ?, ?, ?, ?, ?)",
(client_id, protocol, str(data.get('source_id', '---')), str(data.get('destination_id', '---')), data.get('slot', 0), round(data.get('duration', 0), 1), round(data.get('ber', 0), 2)))
conn.commit()
conn.close()
socketio.emit('dati_aggiornati')
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
app.secret_key = 'ari_fvg_secret_ultra_secure'
client_states = {}
device_configs = {}
client_telemetry = {}
last_notified_errors = {}
device_health = {}
last_seen_reflector = {}
network_mapping = {}
if os.path.exists(CACHE_FILE):
try:
with open(CACHE_FILE, 'r') as f: client_telemetry = json.load(f)
except: client_telemetry = {}
active_calls = {}
with open(CONFIG_PATH) as f: config = json.load(f)
# --- CALLBACKS MQTT ---
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...")
client.subscribe([
("servizi/+/stat", 0),
("dmr-gateway/+/json", 0),
("devices/+/services", 0),
("nxdn-gateway/+/json", 0),
("ysf-gateway/+/json", 0),
("p25-gateway/+/json", 0),
("dstar-gateway/+/json", 0),
("mmdvm/+/json", 0),
("devices/#", 0),
("data/#", 0)
])
else:
logger.error(f"❌ Errore di connessione MQTT. Codice motivo: {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...")
def on_message(client, userdata, msg):
try:
topic = msg.topic
payload = msg.payload.decode().strip()
parts = topic.split('/')
if len(parts) < 2: return
cid = parts[1].lower()
# --- CATTURA CONFIGURAZIONI COMPLETE ---
if parts[0] == 'data' and len(parts) >= 4 and parts[3] == 'full_config':
cid_conf = parts[1].lower()
svc_name = parts[2].lower()
if cid_conf not in device_configs:
device_configs[cid_conf] = {}
try:
device_configs[cid_conf][svc_name] = json.loads(payload)
logger.debug(f"Configurazione salvata per {cid_conf} -> {svc_name}")
except Exception as e:
logger.error(f"Errore parsing config JSON: {e}")
# --- GESTIONE STATI SERVIZIO E NODO ---
elif parts[0] == 'servizi':
client_states[cid] = payload
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
# --- GRILLETTO PUSH: STATO NODO ---
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.")
last_notified_errors[f"{cid}_NODE"] = 'OFFLINE'
elif payload.upper() == 'ONLINE':
if last_notified_errors.get(f"{cid}_NODE") == 'OFFLINE':
broadcast_push_notification(f"🌤️ NODE ONLINE: {cid.upper()}", "Node is back online.")
del last_notified_errors[f"{cid}_NODE"]
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": ""}
save_cache(client_telemetry)
# --- GESTIONE SALUTE DISPOSITIVI ---
elif parts[0] == 'devices' and len(parts) >= 3 and parts[2] == 'services':
try:
data = json.loads(payload)
device_health[cid] = {
"cpu": round(data.get("cpu_usage_percent", 0), 1),
"temp": round(data.get("cpu_temp", 0), 1),
"ram": round(data.get("memory_usage_percent", 0), 1),
"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"})
}
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
# --- GRILLETTO PUSH: SERVIZI IN ERRORE ---
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! ⚠️"
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.")
del last_notified_errors[status_key]
# -----------------------------------------
except Exception as e:
logger.error(f"Errore parsing health: {e}")
# --- GESTIONE DMR GATEWAY ---
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()
data = json.loads(payload)
if cid not in network_mapping:
network_mapping[cid] = {"ts1": "", "ts2": ""}
if str(data.get("Enabled")) == "1":
net_name = data.get("Name", "Net").upper()
is_ts1 = False
is_ts2 = False
keys_to_check = ["PassAllTG", "PassAllPC", "TGRewrite", "PCRewrite", "TypeRewrite", "SrcRewrite"]
for k in keys_to_check:
val = str(data.get(k, "")).strip()
if val.startswith("1"): is_ts1 = True
if val.startswith("2"): is_ts2 = True
if is_ts1: network_mapping[cid]["ts1"] = net_name
if is_ts2: network_mapping[cid]["ts2"] = net_name
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
except Exception as e:
logger.error(f"Errore parsing DMRGateway per {cid}: {e}")
# --- GESTIONE ALTRI GATEWAY ---
elif parts[0] in ['dmr-gateway', 'nxdn-gateway', 'ysf-gateway', 'p25-gateway', 'dstar-gateway']:
data = json.loads(payload)
proto = "DMR"
if "nxdn" in parts[0]: proto = "NXDN"
elif "ysf" in parts[0]: proto = "YSF"
elif "p25" in parts[0]: proto = "P25"
elif "dstar" in parts[0]: proto = "D-STAR"
m = ""
if 'status' in data:
m = data['status'].get('message', '')
elif 'link' in data:
l = data['link']
dest = str(l.get('reflector') or l.get('talkgroup') or '---').strip()
action = l.get('action')
if action == 'linking': last_seen_reflector[f"{cid}_{proto}"] = dest
elif action == 'unlinking': last_seen_reflector[f"{cid}_{proto}"] = "---"
m = f"{'Link' if action=='linking' else 'Unlinked'} {dest}"
if m: save_to_sqlite(cid, {'source_id': "🌐 " + m, 'destination_id': 'NET'}, protocol=proto)
# --- GESTIONE MMDVM E TRAFFICO ---
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}
if 'MMDVM' in data and data['MMDVM'].get('mode') == 'idle':
client_telemetry[cid]["idle"] = True
save_cache(client_telemetry)
return
client_telemetry[cid]["idle"] = False
if 'DMR' in data:
d = data['DMR']
act = d.get('action')
sk = f"ts{d.get('slot', 1)}"
if act in ['start', 'late_entry']:
src = get_call(d.get('source_id'))
dst = str(d.get('destination_id'))
active_calls[cid][sk] = {'src': src, 'dst': dst}
client_telemetry[cid]["alt"] = ""
client_telemetry[cid][sk] = f"🎙️ {src} ➔ TG {dst}"
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
elif act in ['end', 'lost']:
info = active_calls[cid].get(sk, {'src': '---', 'dst': '---'})
d['source_id'], d['destination_id'] = info['src'], info['dst']
save_to_sqlite(cid, d, protocol="DMR")
client_telemetry[cid][sk] = f"{'' if act=='end' else '⚠️'} {info['src']}"
save_cache(client_telemetry)
if sk in active_calls[cid]: del active_calls[cid][sk]
else:
for k, ico, name in [('NXDN','🟢','NXDN'),('YSF','🟣','YSF'),('P25','🟠','P25'),('D-Star','🔵','D-STAR')]:
if k in data:
p = data[k]
act = p.get('action')
if act == 'start':
if k == 'NXDN': src = get_call(p.get('source_id', '---'), 'NXDN')
elif k == 'P25': src = get_call(p.get('source_id', '---'), 'DMR')
else: src = str(p.get('Callsign', p.get('source_cs', p.get('source_info', p.get('source_id', '---')))))
t_list = [p.get('reflector'), p.get('destination_cs'), p.get('destination_id')]
current_target = next((str(x).strip() for x in t_list if x and str(x).strip() not in ['', '---', '0', 'CQCQCQ']), None)
if not current_target or current_target == cid.upper():
target = last_seen_reflector.get(f"{cid}_{name}", "---")
else:
target = current_target
active_calls[cid][k] = {'src': src, 'dst': target}
client_telemetry[cid].update({"ts1":"","ts2":"","alt": f"{ico} {name}: {src}{target}"})
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
elif act in ['end', 'lost']:
info = active_calls[cid].get(k, {'src': '---', 'dst': '---'})
p.update({'source_id': info['src'], 'destination_id': info['dst']})
save_to_sqlite(cid, p, protocol=name)
client_telemetry[cid]["alt"] = f"{'' if act=='end' else '⚠️'} {name}: {info['src']}"
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}")
# --- INIZIALIZZAZIONE CLIENT MQTT ---
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
mqtt_backend.on_disconnect = on_disconnect
mqtt_backend.on_message = on_message
mqtt_backend.connect(config['mqtt']['broker'], config['mqtt']['port'])
mqtt_backend.loop_start()
@app.route('/')
def index(): return render_template('index.html')
@app.route('/api/clients')
def get_clients():
if os.path.exists(CLIENTS_PATH):
with open(CLIENTS_PATH, 'r') as f: return jsonify(json.load(f))
return jsonify([])
@app.route('/api/logs')
def get_logs():
conn = sqlite3.connect(DB_PATH, timeout=10)
c = conn.cursor()
c.execute("SELECT timestamp, client_id, protocol, source_id, target, slot, duration, ber FROM radio_logs ORDER BY id DESC LIMIT 60")
logs = c.fetchall()
conn.close()
return jsonify(logs)
@app.route('/api/states', methods=['GET'])
def get_states():
return jsonify({
"states": client_states,
"telemetry": client_telemetry,
"health": device_health,
"networks": network_mapping
})
@app.route('/api/service_control', methods=['POST'])
def service_control():
if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403
d = request.json
cid = d.get('clientId').lower()
action = d.get('action')
service = d.get('service')
mqtt_backend.publish(f"devices/{cid}/control", f"{action}:{service}")
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT INTO audit_logs (timestamp, username, client_id, command) VALUES (datetime('now','localtime'), ?, ?, ?)",
(session.get('user'), cid, f"SVC_{action.upper()}_{service}"))
conn.commit()
conn.close()
return jsonify({"success": True})
@app.route('/api/login', methods=['POST'])
def login():
d = request.json
username, password = d.get('user'), d.get('pass')
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM users WHERE username = ?", (username,))
user = c.fetchone()
conn.close()
if user and check_password_hash(user['password_hash'], password):
session['logged_in'] = True
session['user'] = user['username']
session['role'] = user['role']
session['allowed_nodes'] = user['allowed_nodes']
return jsonify({"success": True, "role": user['role'], "allowed_nodes": user['allowed_nodes']})
return jsonify({"success": False}), 401
@app.route('/api/command', methods=['POST'])
def cmd():
if not session.get('logged_in'): return jsonify({"success": False, "error": "Non autenticato"}), 403
d = request.json
cid = d['clientId'].lower()
cmd_type = d['type']
username = session.get('user')
role = session.get('role')
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
if is_allowed:
mqtt_backend.publish(f"servizi/{cid}/cmnd", cmd_type)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT INTO audit_logs (timestamp, username, client_id, command) VALUES (datetime('now','localtime'), ?, ?, ?)",
(username, cid, cmd_type))
conn.commit()
conn.close()
client_telemetry[cid] = {"ts1": "🔄 Inviato...", "ts2": "🔄 Inviato...", "alt": ""}
return jsonify({"success": True})
return jsonify({"success": False, "error": "Non hai i permessi per questo nodo."}), 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
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
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT id, username, role, allowed_nodes FROM users")
users = [dict(row) for row in c.fetchall()]
conn.close()
return jsonify(users)
@app.route('/api/users', methods=['POST'])
def add_user():
if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 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"})
h = generate_password_hash(password)
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT INTO users (username, password_hash, role, allowed_nodes) VALUES (?,?,?,?)",
(username, h, role, allowed))
conn.commit()
conn.close()
return jsonify({"success": True})
except sqlite3.IntegrityError:
return jsonify({"success": False, "error": "Username già esistente"})
@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
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!"})
c.execute("DELETE FROM users WHERE id = ?", (user_id,))
conn.commit()
conn.close()
return jsonify({"success": True})
@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
data = request.json
role = data.get('role', 'operator')
allowed = data.get('allowed_nodes', 'all')
password = data.get('password')
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
if password and password.strip() != "":
h = generate_password_hash(password)
c.execute("UPDATE users SET password_hash=?, role=?, allowed_nodes=? WHERE id=?",
(h, role, allowed, user_id))
else:
c.execute("UPDATE users SET role=?, allowed_nodes=? WHERE id=?",
(role, allowed, user_id))
conn.commit()
conn.close()
return jsonify({"success": True})
except Exception as e:
return jsonify({"success": False, "error": str(e)})
@app.route('/api/change_password', methods=['POST'])
def change_password():
if not session.get('logged_in'):
return jsonify({"success": False, "error": "Non autenticato"}), 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
if not new_pass:
return jsonify({"success": False, "error": "La password non può essere vuota"}), 400
h = generate_password_hash(new_pass)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE users SET password_hash = ? WHERE username = ?", (h, user_to_change))
conn.commit()
conn.close()
return jsonify({"success": True})
@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
d = request.json
cmd_type = d.get('type')
clients_list = []
if os.path.exists(CLIENTS_PATH):
with open(CLIENTS_PATH, 'r') as f:
clients_list = json.load(f)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
for client in clients_list:
cid = client['id'].lower()
mqtt_backend.publish(f"servizi/{cid}/cmnd", cmd_type)
c.execute("INSERT INTO audit_logs (timestamp, username, client_id, command) VALUES (datetime('now','localtime'), ?, ?, ?)",
(session.get('user'), cid, f"GLOBAL_OVERRIDE_{cmd_type}"))
conn.commit()
conn.close()
return jsonify({"success": True})
def auto_update_ids():
while True:
try:
with open(CONFIG_PATH, 'r') as f:
current_cfg = json.load(f)
target_time = current_cfg.get("update_schedule", "03:00")
urls = current_cfg.get("id_urls", {
"dmr": "https://radioid.net/static/dmrid.dat",
"nxdn": "https://radioid.net/static/nxdn.csv"
})
now = time.strftime("%H:%M")
if now == target_time:
logger.info(f">>> [AUTO-UPDATE] Orario raggiunto ({now}). Download in corso...")
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.")
time.sleep(65)
except Exception as e:
logger.error(f">>> [AUTO-UPDATE] Errore: {e}")
time.sleep(30)
@app.route('/api/ui_config', methods=['GET'])
def get_ui_config():
try:
with open(CONFIG_PATH, 'r') as f:
cfg = json.load(f)
ui_cfg = cfg.get("ui", {
"profileA_Name": "PROFILO A",
"profileA_Color": "#3498db",
"profileB_Name": "PROFILO B",
"profileB_Color": "#9b59b6"
})
return jsonify(ui_cfg)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/config', methods=['GET'])
def get_config_api():
if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403
with open(CONFIG_PATH, 'r') as f:
cfg = json.load(f)
return jsonify({
"update_schedule": cfg.get("update_schedule", "03:00"),
"url_dmr": cfg.get("id_urls", {}).get("dmr", ""),
"url_nxdn": cfg.get("id_urls", {}).get("nxdn", "")
})
@app.route('/api/config', methods=['POST'])
def save_config_api():
if session.get('role') != 'admin': return jsonify({"error": "Non autorizzato"}), 403
new_data = request.json
with open(CONFIG_PATH, 'r') as f:
cfg = json.load(f)
cfg["update_schedule"] = new_data.get("update_schedule", "03:00")
if "id_urls" not in cfg: cfg["id_urls"] = {}
cfg["id_urls"]["dmr"] = new_data.get("url_dmr", "")
cfg["id_urls"]["nxdn"] = new_data.get("url_nxdn", "")
with open(CONFIG_PATH, 'w') as f:
json.dump(cfg, f, indent=4)
return jsonify({"success": True})
@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
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({"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
d = request.json
cid = d.get('clientId').lower()
service = d.get('service').lower()
new_config = d.get('config_data')
topic_set = f"devices/{cid}/config_set/{service}"
mqtt_backend.publish(topic_set, json.dumps(new_config))
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT INTO audit_logs (timestamp, username, client_id, command) VALUES (datetime('now','localtime'), ?, ?, ?)",
(session.get('user'), cid, f"EDIT_CONFIG_{service.upper()}"))
conn.commit()
conn.close()
return jsonify({"success": True})
@app.route('/manifest.json')
def serve_manifest():
return send_from_directory('.', 'manifest.json')
@app.route('/sw.js')
def serve_sw():
return send_from_directory('.', 'sw.js')
@app.route('/icon-512.png')
def serve_icon():
return send_from_directory('.', 'icon-512.png')
def broadcast_push_notification(title, body):
wp_config = config.get('webpush')
if not wp_config: return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id, subscription FROM push_subscriptions")
subs = c.fetchall()
for sub_id, sub_json in subs:
try:
webpush(
subscription_info=json.loads(sub_json),
data=json.dumps({"title": title, "body": body}),
vapid_private_key=wp_config['vapid_private_key'],
vapid_claims={"sub": wp_config['vapid_claim_email']}
)
except WebPushException as ex:
if ex.response and ex.response.status_code == 410:
c.execute("DELETE FROM push_subscriptions WHERE id = ?", (sub_id,))
conn.commit()
except Exception as e:
logger.error(f"Errore generico Push: {e}")
conn.close()
@app.route('/api/vapid_public_key')
def get_vapid_key():
return jsonify({"public_key": config.get('webpush', {}).get('vapid_public_key', '')})
@app.route('/api/subscribe', methods=['POST'])
def subscribe_push():
if not session.get('logged_in'): return jsonify({"error": "Unauthorized"}), 403
sub_data = request.json
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT OR IGNORE INTO push_subscriptions (username, subscription) VALUES (?, ?)",
(session.get('user'), json.dumps(sub_data)))
conn.commit()
conn.close()
return jsonify({"success": True})
if __name__ == '__main__':
threading.Thread(target=auto_update_ids, daemon=True).start()
socketio.run(app, host='0.0.0.0', port=9000, allow_unsafe_werkzeug=True)