From efa02394dcc7fc4430f5e28fbc2b809584efebb2 Mon Sep 17 00:00:00 2001 From: Roby Date: Sun, 26 Apr 2026 01:36:09 +0200 Subject: [PATCH] Initial commit --- .gitignore | 26 + README.md | 58 ++ app.py | 784 +++++++++++++++++++++ clients.json.example | 6 + config.json.example | 28 + icon-512.png | Bin 0 -> 6530 bytes images/dashboard.png | Bin 0 -> 89672 bytes install.txt | 106 +++ manifest.json | 15 + requirements-server.txt | 8 + sw.js | 63 ++ systemd/fleet-control.service | 19 + templates/index.html | 1230 +++++++++++++++++++++++++++++++++ test-push.py | 11 + 14 files changed, 2354 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 clients.json.example create mode 100644 config.json.example create mode 100644 icon-512.png create mode 100644 images/dashboard.png create mode 100644 install.txt create mode 100644 manifest.json create mode 100644 requirements-server.txt create mode 100644 sw.js create mode 100644 systemd/fleet-control.service create mode 100644 templates/index.html create mode 100644 test-push.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..071f847 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Ignora le cartelle temporanee di Python +__pycache__/ +*.pyc + +# Ignora il database SQLite (non vogliamo pubblicare i log e le password hashate!) +*.db +monitor.db +monitor.db-journal +monitor.db-shm +monitor.db-wal + +# Ignora i file di configurazione reali (pubblicherai solo i .example.json) +config.json +clients.json + +# Ignora i file dati scaricati in automatico (sono grandi e dinamici) +dmrid.dat +nxdn.csv +telemetry_cache.json + +# Ignora log di sistema eventuali +*.log + +# Ignora le configurazioni reali dell'agente remoto +*.ini +!*.example.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe34289 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# ๐Ÿ“ก Fleet Control Console (Server) + +๐ŸŒ *[Read in English](#english) | ๐Ÿ‡ฎ๐Ÿ‡น [Leggi in Italiano](#italiano)* + +--- + + +## ๐Ÿ‡ฌ๐Ÿ‡ง English + +**Fleet Control Console** is a professional, real-time command and control (C2) dashboard designed for amateur radio repeater networks (MMDVM). + +![Dashboard Screenshot](images/dashboard.png) + +### ๐Ÿค– Remote Agent +To monitor your remote nodes (Raspberry Pi), download the dedicated lightweight agent here: +`[Insert your Agent Repository URL here]` + +### โœจ Features +* **Zero-Latency Real-Time UI:** Powered by WebSockets (Socket.IO). +* **Web Push Notifications:** Instant alerts on desktop or mobile. +* **Centralized Telemetry & Service Management.** +* **Global Operations:** Switch profiles instantly. + +### ๐Ÿš€ Installation & Setup +1. Read `install.txt` to install system prerequisites (compilers). +2. `python3 -m venv venv && source venv/bin/activate` +3. `pip install -r requirements.txt` +4. Configure `config.json` and generate VAPID keys. +5. Run via Gunicorn: `gunicorn -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -w 1 --bind 0.0.0.0:9000 app:app` + +--- + + +## ๐Ÿ‡ฎ๐Ÿ‡น Italiano + +**Fleet Control Console** รจ una dashboard di comando e controllo (C2) professionale in tempo reale per le reti di ripetitori radioamatoriali (MMDVM). + +![Schermata Dashboard](images/dashboard.png) + +### ๐Ÿค– Agente Remoto +Per monitorare i tuoi nodi remoti (Raspberry Pi), scarica l'agente dedicato qui: +`[Inserisci qui l'URL del tuo Repository Agent]` + +### โœจ Funzionalitร  +* **Interfaccia Real-Time a Latenza Zero** tramite WebSockets. +* **Notifiche Push Web** per allarmi critici. +* **Telemetria Centralizzata e Gestione Servizi.** +* **Operazioni Globali** su tutta la rete. + +### ๐Ÿš€ Installazione +1. Leggi `install.txt` per i requisiti di sistema (compilatori Linux). +2. `python3 -m venv venv && source venv/bin/activate` +3. `pip install -r requirements.txt` +4. Configura `config.json` con credenziali MQTT e chiavi VAPID. +5. Avvia con Gunicorn. + +--- +*Created by IV3JDV @ ARIFVG - 2026* diff --git a/app.py b/app.py new file mode 100644 index 0000000..6e5b7bb --- /dev/null +++ b/app.py @@ -0,0 +1,784 @@ +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 + +# --- LOGGING CONFIGURATION --- +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") +# Silence HTTP request spam (GET /api/states 200 OK) +logging.getLogger('werkzeug').setLevel(logging.ERROR) + +# --- PATHS --- +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;') # <-- MAGIC: Enable simultaneous read/write! + + 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 + + try: + c.execute("ALTER TABLE radio_logs ADD COLUMN source_ext TEXT DEFAULT ''") + 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: + # Default value if missing config file + def_user = "admin" + def_pass = "admin123" + + # Try read config.json + try: + with open(CONFIG_PATH, 'r') as f: + cfg = json.load(f) + def_user = cfg.get("web_admin", {}).get("default_user", "admin") + def_pass = cfg.get("web_admin", {}).get("default_pass", "admin123") + except Exception: + pass + + h = generate_password_hash(def_pass) + c.execute("INSERT INTO users (username, password_hash, role, allowed_nodes) VALUES (?,?,?,?)", + (def_user, h, 'admin', 'all')) + logger.info(f">>> DEFAULT USER CREATED - User: {def_user} | Pass: {def_pass} <<<") + + conn.commit() + conn.close() + +init_db() + +# --- ID DATABASE LOADING --- +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, source_ext) 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), str(data.get('source_ext', '')))) + conn.commit() + conn.close() + socketio.emit('dati_aggiornati') + +app = Flask(__name__) +socketio = SocketIO(app, cors_allowed_origins="*") +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) + +# --- MQTT CALLBACKS --- +def on_connect(client, userdata, flags, reason_code, properties=None): + if reason_code == 0: + logger.info("โœ… Successfully connected to MQTT Broker! Subscribing to topics...") + 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"โŒ MQTT Connection Error. Reason code: {reason_code}") + +def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None): + logger.warning(f"โš ๏ธ MQTT Disconnection detected! Reason code: {reason_code}. Attempting automatic reconnection...") + +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() + + # --- 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() + if cid_conf not in device_configs: + device_configs[cid_conf] = {} + try: + device_configs[cid_conf][svc_name] = json.loads(payload) + logger.debug(f"Configuration saved for {cid_conf} -> {svc_name}") + except Exception as e: + logger.error(f"Error parsing config JSON: {e}") + + # --- NODE AND SERVICE STATE MANAGEMENT --- + elif parts[0] == 'servizi': + client_states[cid] = payload + socketio.emit('dati_aggiornati') # <--- WEBSOCKET + + # --- 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.") + 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": "Waiting...", "ts2": "Waiting...", "alt": ""} + save_cache(client_telemetry) + + # --- DEVICE HEALTH MANAGEMENT --- + 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": "PROFILE A", "B": "PROFILE B"}) + } + socketio.emit('dati_aggiornati') # <--- WEBSOCKET + + # --- 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"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.") + del last_notified_errors[status_key] + # ----------------------------------------- + + except Exception as e: + logger.error(f"Error parsing health data: {e}") + + # --- 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() + 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"Error parsing DMRGateway for {cid}: {e}") + + # --- OTHER GATEWAYS MANAGEMENT --- + 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) + + # --- 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": "Waiting...", "ts2": "Waiting...", "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, 'ext': str(p.get('source_ext', ''))} + 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': '---', 'ext': ''}) + p.update({'source_id': info['src'], 'destination_id': info['dst'], 'source_ext': info['ext']}) + 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"MQTT MSG ERROR: {e}") + +# --- 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 +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, source_ext 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/stats') +def get_stats(): + # Leggiamo il parametro 'node' (di default 'all') + node = request.args.get('node', 'all').lower() + + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + + # Prepariamo il filtro aggiuntivo se รจ stato selezionato un nodo + node_filter = "" + params = [] + if node != 'all': + node_filter = " AND LOWER(client_id) = ?" + params.append(node) + + # 1. Top 5 TalkGroups di OGGI + c.execute(f"""SELECT target, COUNT(*) as cnt FROM radio_logs + WHERE target NOT IN ('---', 'NET', '') + AND date(timestamp) = date('now', 'localtime'){node_filter} + GROUP BY target ORDER BY cnt DESC LIMIT 5""", params) + top_tgs = [{"target": row[0], "count": row[1]} for row in c.fetchall()] + + # 2. Top 5 Callsign di OGGI + c.execute(f"""SELECT source_id, COUNT(*) as cnt FROM radio_logs + WHERE source_id NOT LIKE '๐ŸŒ%' + AND date(timestamp) = date('now', 'localtime'){node_filter} + GROUP BY source_id ORDER BY cnt DESC LIMIT 5""", params) + top_calls = [{"call": row[0], "count": row[1]} for row in c.fetchall()] + + # 3. Tempo medio dei transiti di OGGI + c.execute(f"""SELECT AVG(duration) FROM radio_logs + WHERE duration > 0.5 + AND date(timestamp) = date('now', 'localtime'){node_filter}""", params) + avg_dur = c.fetchone()[0] + avg_dur = round(avg_dur, 1) if avg_dur else 0 + + # 4. Totale transiti di OGGI + c.execute(f"SELECT COUNT(*) FROM radio_logs WHERE date(timestamp) = date('now', 'localtime'){node_filter}", params) + today_tx = c.fetchone()[0] + + conn.close() + return jsonify({ + "top_tgs": top_tgs, + "top_calls": top_calls, + "avg_duration": avg_dur, + "today_tx": today_tx + }) + +@app.route('/api/service_control', methods=['POST']) +def service_control(): + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 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": "Not authenticated"}), 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": "Only Admins can reboot."}), 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": "๐Ÿ”„ Sent...", "ts2": "๐Ÿ”„ Sent...", "alt": ""} + return jsonify({"success": True}) + return jsonify({"success": False, "error": "You do not have permission for this node."}), 403 + +@app.route('/api/update_nodes', methods=['POST']) +def update_nodes(): + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 + mqtt_backend.publish("devices/control/request", "update") + return jsonify({"success": True}) + + # Mandiamo il comando "update" direttamente nel topic privato di ciascun nodo + for client in clients_list: + cid = client['id'].lower() + mqtt_backend.publish(f"devices/{cid}/control", "update", qos=1) + + logger.info("๐Ÿ“ข Inviato comando REQ CONFIG diretto a tutti i nodi della flotta.") + return jsonify({"success": True}) + +@app.route('/api/users', methods=['GET']) +def get_users(): + if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 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": "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": "Missing data"}) + 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 already exists"}) + +@app.route('/api/users/', methods=['DELETE']) +def delete_user(user_id): + 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": "You cannot delete yourself!"}) + c.execute("DELETE FROM users WHERE id = ?", (user_id,)) + conn.commit() + conn.close() + return jsonify({"success": True}) + +@app.route('/api/users/', methods=['PUT']) +def update_user(user_id): + if session.get('role') != 'admin': + return jsonify({"error": "Unauthorized"}), 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": "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": "Unauthorized"}), 403 + if not new_pass: + return jsonify({"success": False, "error": "Password cannot be empty"}), 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": "Admin action only!"}), 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] 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] Completed successfully.") + time.sleep(65) + except Exception as e: + logger.error(f">>> [AUTO-UPDATE] Error: {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": "PROFILE A", + "profileA_Color": "#3498db", + "profileB_Name": "PROFILE 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": "Unauthorized"}), 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": "Unauthorized"}), 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//', methods=['GET']) +def get_config_file(cid, service): + 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": "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": "Unauthorized"}), 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"Generic Push Error: {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}) + +threading.Thread(target=auto_update_ids, daemon=True).start() + +if __name__ == '__main__': + socketio.run(app, host='0.0.0.0', port=9000) diff --git a/clients.json.example b/clients.json.example new file mode 100644 index 0000000..c596588 --- /dev/null +++ b/clients.json.example @@ -0,0 +1,6 @@ +[ + {"_comment": "Enter each repeater/node you want to control. The ID must match the MQTT topic"}, + { "id": "repeater1", "name": "NAME 1" }, + { "id": "repeater2", "name": "NAME 2" }, + { "id": "repeater3", "name": "NAME 3" } +] diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..510e0fd --- /dev/null +++ b/config.json.example @@ -0,0 +1,28 @@ +{ + "mqtt": { + "broker": "your_mqtt_broker_address", + "port": 1883, + "user": "your_username", + "password": "your_password" + }, + "web_admin": { + "default_user": "admin", + "default_pass": "admin123" + }, + "webpush": { + "vapid_public_key": "INSERT_GENERATED_PUBLIC_KEY_HERE", + "vapid_private_key": "INSERT_GENERATED_PRIVATE_KEY_HERE", + "vapid_claim_email": "mailto:your@email.com" + }, + "ui": { + "profileA_Name": "PROFILE A", + "profileA_Color": "#3b82f6", + "profileB_Name": "PROFILE B", + "profileB_Color": "#eab308" + }, + "update_schedule": "03:00", + "id_urls": { + "dmr": "https://radioid.net/static/dmrid.dat", + "nxdn": "https://radioid.net/static/nxdn.csv" + } +} diff --git a/icon-512.png b/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..5bb7d6790f7595b7cb8cf7e4feec60897a419654 GIT binary patch literal 6530 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 literal 0 HcmV?d00001 diff --git a/images/dashboard.png b/images/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..6f85f7bbef30fde8b6d073083d960e798c422744 GIT binary patch literal 89672 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�|xpFH?RlrYKP}}Z<|Kyde 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 { + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => cache.addAll(urlsToCache)) + ); +}); + +self.addEventListener('fetch', event => { + event.respondWith( + fetch(event.request).catch(() => caches.match(event.request)) + ); +}); + +// --- 2. GESTIONE NOTIFICHE PUSH (Il nuovo codice) --- +self.addEventListener('push', function(event) { + console.log('[Service Worker] Notifica Push ricevuta.'); + + let data = { title: 'Fleet Alert', body: 'Nuovo messaggio dal sistema.' }; + + if (event.data) { + try { + data = event.data.json(); + } catch (e) { + data.body = event.data.text(); + } + } + + const options = { + body: data.body, + icon: '/icon-512.png', + badge: '/icon-512.png', + vibrate: [200, 100, 200], + data: { + dateOfArrival: Date.now(), + primaryKey: '1' + }, + actions: [ + { action: 'explore', title: 'Apri Dashboard' }, + { action: 'close', title: 'Chiudi' } + ] + }; + + event.waitUntil( + self.registration.showNotification(data.title, options) + ); +}); + +self.addEventListener('notificationclick', function(event) { + event.notification.close(); + if (event.action === 'explore') { + event.waitUntil( + clients.openWindow('/') + ); + } +}); diff --git a/systemd/fleet-control.service b/systemd/fleet-control.service new file mode 100644 index 0000000..d00d798 --- /dev/null +++ b/systemd/fleet-control.service @@ -0,0 +1,19 @@ +[Unit] +Description=Fleet Control Console - Central Hub +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/web-control +ExecStart=/usr/local/bin/gunicorn -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -w 1 --graceful-timeout 2 --bind 0.0.0.0:9000 app:app +TimeoutStopSec=3 +Restart=always +RestartSec=5 + +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=fleet-console + +[Install] +WantedBy=multi-user.target diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9ef8e54 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,1230 @@ + + + + + + + Fleet Control Console + + + + + + + + + + + +
+
+
FLEET CONTROL CONSOLE
+
+ + +
+
+
+
+ +
+ +
+

+ ๐Ÿ“Š DAILY STATISTICS +

+ +
+ +
+
+

๐ŸŽฏ TOP TALKGROUPS

+
Loading...
+
+ +
+

๐Ÿ—ฃ๏ธ TOP CALLSIGNS

+
Loading...
+
+ +
+
+
--s
+
โฑ๏ธ AVERAGE DURATION
+
+
+
+
--
+
๐Ÿ“ก TRANSITS TODAY
+
+
+
+ +
+

+ ๐Ÿ“ก Lastheard +

+
+ + + + + + + + + + + + + + +
TimeRepeaterModeSlotCallsignTarget / TGDurationBER
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-push.py b/test-push.py new file mode 100644 index 0000000..f44a7c5 --- /dev/null +++ b/test-push.py @@ -0,0 +1,11 @@ +import sqlite3 +import json +from app import broadcast_push_notification, init_db + +# Proviamo a inviare una notifica a tutti gli iscritti nel DB +print("๐Ÿš€ Starting notification test...") +broadcast_push_notification( + "โš ๏ธ ALLERTA FLOTTA", + "Test Push Notification: System Online!" +) +print("โœ… Command sent.")