Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
53c4ddf6f5
|
|||
|
30b30c626c
|
|||
|
ca7547adcf
|
|||
|
817eda7d03
|
|||
|
56cd9d5323
|
|||
|
47f1490701
|
|||
| 33dac9c8e4 | |||
|
6cdfbedf9e
|
|||
|
e4d1e3eca4
|
|||
|
ad961dedc2
|
|||
|
289b6d5f59
|
|||
|
8881f6630f
|
|||
|
0a7f7f718e
|
|||
|
2685ee0212
|
|||
|
8f48bfaa5a
|
|||
|
2a722a7ae6
|
|||
|
cd3960b110
|
|||
|
bc7f7fe345
|
@@ -20,6 +20,7 @@ telemetry_cache.json
|
|||||||
|
|
||||||
# Ignora log di sistema eventuali
|
# Ignora log di sistema eventuali
|
||||||
*.log
|
*.log
|
||||||
|
*.log.*
|
||||||
|
|
||||||
# Ignora le configurazioni reali dell'agente remoto
|
# Ignora le configurazioni reali dell'agente remoto
|
||||||
*.ini
|
*.ini
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
<a name="english"></a>
|
<a name="english"></a>
|
||||||
## 🇬🇧 English
|
## 🇬🇧 English
|
||||||
|
|
||||||
**Fleet Control Console** is a professional, real-time command and control (C2) dashboard designed for amateur radio repeater networks (MMDVM). This repository contains the **Server (Central Hub)** component, which provides a centralized web interface to monitor and manage a fleet of remote digital voice nodes (DMR, NXDN, YSF, P25).
|
**Fleet Control Console** is a professional, real-time command and control (C2) dashboard designed for amateur radio repeater networks (MMDVM). This repository contains the **Server (Central Hub)** component, which provides a centralized web interface to monitor and manage a fleet of remote digital voice nodes (DMR, NXDN, YSF, P25).
|
||||||
|
|
||||||
> ℹ️ **Note:** This is the Central Server repository. To monitor remote repeaters, you must install the [Fleet Control Agent](link_al_repo_agent_qui) on each node.
|
> ℹ️ **Note:** This is the Central Server repository. To monitor remote repeaters, you must install the [Fleet Control Agent](https://git.arifvg.it/iv3jdv/fleet-node-agent.git) on each node.
|
||||||
|
|
||||||
### 🏗️ Architecture & Technology Stack
|
### 🏗️ Architecture & Technology Stack
|
||||||
The server acts as the brain of the network:
|
The server acts as the brain of the network:
|
||||||
@@ -31,6 +33,10 @@ The server acts as the brain of the network:
|
|||||||
* A critical system daemon crashes (Auto-healing failed).
|
* A critical system daemon crashes (Auto-healing failed).
|
||||||
* Works securely even when the web app is closed or in the background.
|
* Works securely even when the web app is closed or in the background.
|
||||||
|
|
||||||
|
### ⚠️ Security Requirement: HTTPS
|
||||||
|
Please note that **Web Push Notifications** and the **Service Worker** require a secure connection to function. For security reasons, modern browsers only enable these features over **HTTPS**. If you are accessing the console via HTTP (outside of `localhost`), the "Push" button and notification registration will not work. We recommend using a reverse proxy (like Nginx, Caddy, or Traefik) with an SSL certificate.
|
||||||
|
**Pro Tip:** You can use Let's Encrypt to get a free SSL certificate for your domain.
|
||||||
|
|
||||||
#### 🛠️ Advanced Remote Control & Maintenance
|
#### 🛠️ Advanced Remote Control & Maintenance
|
||||||
* **Remote .INI Editor:** Edit daemon configuration files (e.g., MMDVMHost.ini) directly from the web interface without SSH access.
|
* **Remote .INI Editor:** Edit daemon configuration files (e.g., MMDVMHost.ini) directly from the web interface without SSH access.
|
||||||
* **Service Management:** Start, Stop, or Restart remote system daemons with a single click.
|
* **Service Management:** Start, Stop, or Restart remote system daemons with a single click.
|
||||||
@@ -49,16 +55,16 @@ The server acts as the brain of the network:
|
|||||||
|
|
||||||
### 🚀 Installation Guide
|
### 🚀 Installation Guide
|
||||||
|
|
||||||
#### 0. System Pre-requisites (Critical)
|
#### 0. Root Privileges
|
||||||
Before installing Python dependencies, install the necessary system compilers and pip/venv tools. On Debian/Ubuntu:
|
All installation steps must be executed as the `root` user. Before starting, elevate your privileges by running:
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
apt update
|
||||||
sudo apt install build-essential python3-dev libssl-dev libffi-dev python3-pip python3-venv
|
apt install build-essential python3-dev libssl-dev libffi-dev python3-pip python3-venv
|
||||||
```
|
```
|
||||||
#### 1. Clone Repository
|
#### 1. Clone Repository
|
||||||
Clone the repository into the /opt directory to ensure all systemd paths work correctly:
|
Clone the repository into the /opt directory to ensure all systemd paths work correctly:
|
||||||
```bash
|
```bash
|
||||||
sudo git clone https://tuo-gitea.com/utente/fleet-control-server.git /opt/fleet-control-server
|
git clone https://tuo-gitea.com/utente/fleet-control-server.git /opt/fleet-control-server
|
||||||
cd /opt/fleet-control-server
|
cd /opt/fleet-control-server
|
||||||
```
|
```
|
||||||
#### 2. Virtual Environment Setup (Recommended)
|
#### 2. Virtual Environment Setup (Recommended)
|
||||||
@@ -80,10 +86,10 @@ pip install -r requirements.txt
|
|||||||
#### 4. Running as a Service
|
#### 4. Running as a Service
|
||||||
To run the server continuously in production using Gunicorn:
|
To run the server continuously in production using Gunicorn:
|
||||||
```bash
|
```bash
|
||||||
sudo cp fleet-console.service /etc/systemd/system/
|
cp fleet-console.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
sudo systemctl enable fleet-console
|
systemctl enable fleet-console
|
||||||
sudo systemctl start fleet-console
|
systemctl start fleet-console
|
||||||
```
|
```
|
||||||
*(Ensure the `.service` file points to the `gunicorn` executable inside your `venv`)*.
|
*(Ensure the `.service` file points to the `gunicorn` executable inside your `venv`)*.
|
||||||
|
|
||||||
@@ -94,7 +100,7 @@ sudo systemctl start fleet-console
|
|||||||
|
|
||||||
**Fleet Control Console** è una dashboard di comando e controllo (C2) professionale in tempo reale, progettata per le reti di ripetitori radioamatoriali (MMDVM). Questo repository contiene il **Server (Central Hub)**, che fornisce un'interfaccia web centralizzata per monitorare e gestire una flotta di nodi digitali remoti (DMR, NXDN, YSF, P25).
|
**Fleet Control Console** è una dashboard di comando e controllo (C2) professionale in tempo reale, progettata per le reti di ripetitori radioamatoriali (MMDVM). Questo repository contiene il **Server (Central Hub)**, che fornisce un'interfaccia web centralizzata per monitorare e gestire una flotta di nodi digitali remoti (DMR, NXDN, YSF, P25).
|
||||||
|
|
||||||
> ℹ️ **Nota:** Questo è il repository del Server Centrale. Per monitorare i ripetitori, devi installare il [Fleet Control Agent](link_al_repo_agent_qui) su ciascun nodo remoto.
|
> ℹ️ **Nota:** Questo è il repository del Server Centrale. Per monitorare i ripetitori, devi installare il [Fleet Control Agent](https://git.arifvg.it/iv3jdv/fleet-node-agent.git) su ciascun nodo remoto.
|
||||||
|
|
||||||
### 🏗️ Architettura e Tecnologie
|
### 🏗️ Architettura e Tecnologie
|
||||||
Il server agisce da cervello della rete:
|
Il server agisce da cervello della rete:
|
||||||
@@ -116,6 +122,10 @@ Il server agisce da cervello della rete:
|
|||||||
* Un demone di sistema remoto si blocca (fallimento auto-healing).
|
* Un demone di sistema remoto si blocca (fallimento auto-healing).
|
||||||
* Funzionano in modo sicuro anche quando la web app è chiusa o in background.
|
* Funzionano in modo sicuro anche quando la web app è chiusa o in background.
|
||||||
|
|
||||||
|
### ⚠️ Sicurezza Richiesta: HTTPS
|
||||||
|
Si prega di notare che le **Notifiche Web Push** e il **Service Worker** richiedono una connessione sicura per funzionare. Per motivi di sicurezza, i browser moderni abilitano queste funzionalità solo su **HTTPS**. Se accedi alla console via HTTP (al di fuori di `localhost`), il tasto "Push" e la registrazione delle notifiche non saranno attivi. Si consiglia l'uso di un reverse proxy (come Nginx, Caddy o Traefik) con certificato SSL.
|
||||||
|
**Suggerimento:** Puoi usare Let's Encrypt per ottenere un certificato SSL gratuito per il tuo dominio.
|
||||||
|
|
||||||
#### 🛠️ Controllo Remoto & Manutenzione Avanzata
|
#### 🛠️ Controllo Remoto & Manutenzione Avanzata
|
||||||
* **Editor .INI Remoto:** Modifica i file di configurazione (es. MMDVMHost.ini) direttamente dal pannello web, senza bisogno di accessi SSH.
|
* **Editor .INI Remoto:** Modifica i file di configurazione (es. MMDVMHost.ini) direttamente dal pannello web, senza bisogno di accessi SSH.
|
||||||
* **Gestione Demoni:** Avvia, arresta o riavvia i servizi di sistema remoti con un clic.
|
* **Gestione Demoni:** Avvia, arresta o riavvia i servizi di sistema remoti con un clic.
|
||||||
@@ -134,16 +144,16 @@ Il server agisce da cervello della rete:
|
|||||||
|
|
||||||
### 🚀 Guida all'Installazione
|
### 🚀 Guida all'Installazione
|
||||||
|
|
||||||
#### 0. Requisiti di Sistema (Critici)
|
#### 0. Privilegi di Root
|
||||||
Prima di installare le dipendenze Python, installa i compilatori di base e gli strumenti per gli ambienti virtuali. Su Debian/Ubuntu:
|
Tutti i passaggi di installazione devono essere eseguiti come utente `root`. Prima di iniziare, eleva i tuoi privilegi eseguendo:
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
apt update
|
||||||
sudo apt install build-essential python3-dev libssl-dev libffi-dev python3-pip python3-venv
|
apt install build-essential python3-dev libssl-dev libffi-dev python3-pip python3-venv
|
||||||
```
|
```
|
||||||
#### 1. Clonazione dei Repository
|
#### 1. Clonazione dei Repository
|
||||||
Clona il repository nella cartella /opt per assicurarti che tutti i percorsi dei servizi systemd siano corretti:
|
Clona il repository nella cartella /opt per assicurarti che tutti i percorsi dei servizi systemd siano corretti:
|
||||||
```bash
|
```bash
|
||||||
sudo git clone https://tuo-gitea.com/utente/fleet-control-server.git /opt/fleet-control-server
|
git clone https://tuo-gitea.com/utente/fleet-control-server.git /opt/fleet-control-server
|
||||||
cd /opt/fleet-control-server
|
cd /opt/fleet-control-server
|
||||||
```
|
```
|
||||||
#### 2. Setup Ambiente Virtuale (Consigliato)
|
#### 2. Setup Ambiente Virtuale (Consigliato)
|
||||||
@@ -165,10 +175,10 @@ pip install -r requirements.txt
|
|||||||
#### 4. Esecuzione come Servizio (systemd)
|
#### 4. Esecuzione come Servizio (systemd)
|
||||||
Per eseguire il server in produzione in modo continuo e stabile con Gunicorn:
|
Per eseguire il server in produzione in modo continuo e stabile con Gunicorn:
|
||||||
```bash
|
```bash
|
||||||
sudo cp fleet-console.service /etc/systemd/system/
|
cp fleet-console.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
sudo systemctl enable fleet-console
|
systemctl enable fleet-console
|
||||||
sudo systemctl start fleet-console
|
systemctl start fleet-console
|
||||||
```
|
```
|
||||||
*(Assicurati che il file `.service` punti all'eseguibile `gunicorn` situato all'interno della cartella `venv`).*
|
*(Assicurati che il file `.service` punti all'eseguibile `gunicorn` situato all'interno della cartella `venv`).*
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ last_notified_errors = {}
|
|||||||
device_health = {}
|
device_health = {}
|
||||||
last_seen_reflector = {}
|
last_seen_reflector = {}
|
||||||
network_mapping = {}
|
network_mapping = {}
|
||||||
|
node_info = {}
|
||||||
|
node_general = {}
|
||||||
|
|
||||||
if os.path.exists(CACHE_FILE):
|
if os.path.exists(CACHE_FILE):
|
||||||
try:
|
try:
|
||||||
@@ -170,18 +172,22 @@ def on_connect(client, userdata, flags, reason_code, properties=None):
|
|||||||
logger.info("✅ Successfully connected to MQTT Broker! Subscribing to topics...")
|
logger.info("✅ Successfully connected to MQTT Broker! Subscribing to topics...")
|
||||||
# Invia lo stato Online ai client web
|
# Invia lo stato Online ai client web
|
||||||
socketio.emit('mqtt_status', {'connected': True})
|
socketio.emit('mqtt_status', {'connected': True})
|
||||||
client.subscribe([
|
|
||||||
("servizi/+/stat", 0),
|
# --- LETTURA DINAMICA DEI TOPIC ---
|
||||||
("dmr-gateway/+/json", 0),
|
default_topics = [
|
||||||
("devices/+/services", 0),
|
"servizi/+/stat", "dmr-gateway/+/json", "devices/+/services",
|
||||||
("nxdn-gateway/+/json", 0),
|
"nxdn-gateway/+/json", "ysf-gateway/+/json", "p25-gateway/+/json",
|
||||||
("ysf-gateway/+/json", 0),
|
"dstar-gateway/+/json", "mmdvm/+/json", "devices/#", "data/#"
|
||||||
("p25-gateway/+/json", 0),
|
]
|
||||||
("dstar-gateway/+/json", 0),
|
|
||||||
("mmdvm/+/json", 0),
|
# Cerca la lista "topics" nel config.json, se non la trova usa quella di default
|
||||||
("devices/#", 0),
|
topics_list = config.get('mqtt', {}).get('topics', default_topics)
|
||||||
("data/#", 0)
|
|
||||||
])
|
# Converte la lista di stringhe nel formato richiesto da paho-mqtt: [(topic, qos), (topic, qos)...]
|
||||||
|
subscribe_list = [(topic, 0) for topic in topics_list]
|
||||||
|
|
||||||
|
client.subscribe(subscribe_list)
|
||||||
|
logger.info(f"Subscribed to {len(subscribe_list)} MQTT topics.")
|
||||||
else:
|
else:
|
||||||
mqtt_connected_status = False
|
mqtt_connected_status = False
|
||||||
socketio.emit('mqtt_status', {'connected': False})
|
socketio.emit('mqtt_status', {'connected': False})
|
||||||
@@ -205,10 +211,17 @@ def on_message(client, userdata, msg):
|
|||||||
payload = msg.payload.decode().strip()
|
payload = msg.payload.decode().strip()
|
||||||
parts = topic.split('/')
|
parts = topic.split('/')
|
||||||
if len(parts) < 2: return
|
if len(parts) < 2: return
|
||||||
|
|
||||||
cid = parts[1].lower()
|
cid = parts[1].lower()
|
||||||
|
|
||||||
|
# --- MAGIA DELLA NORMALIZZAZIONE ---
|
||||||
|
# Prendiamo i blocchi del topic, li facciamo minuscoli e togliamo i trattini
|
||||||
|
# Così "DMRGateway", "dmr-gateway" e "dmrgateway" diventano tutti identici per noi
|
||||||
|
p0 = parts[0].lower().replace('-', '')
|
||||||
|
p2 = parts[2].lower().replace('-', '') if len(parts) > 2 else ""
|
||||||
|
|
||||||
# --- CAPTURE FULL CONFIGURATIONS ---
|
# --- CAPTURE FULL CONFIGURATIONS ---
|
||||||
if parts[0] == 'data' and len(parts) >= 4 and parts[3] == 'full_config':
|
if p0 == 'data' and len(parts) >= 4 and parts[3].lower() == 'full_config':
|
||||||
cid_conf = parts[1].lower()
|
cid_conf = parts[1].lower()
|
||||||
svc_name = parts[2].lower()
|
svc_name = parts[2].lower()
|
||||||
if cid_conf not in device_configs:
|
if cid_conf not in device_configs:
|
||||||
@@ -220,7 +233,7 @@ def on_message(client, userdata, msg):
|
|||||||
logger.error(f"Error parsing config JSON: {e}")
|
logger.error(f"Error parsing config JSON: {e}")
|
||||||
|
|
||||||
# --- NODE AND SERVICE STATE MANAGEMENT ---
|
# --- NODE AND SERVICE STATE MANAGEMENT ---
|
||||||
elif parts[0] == 'servizi':
|
elif p0 == 'servizi':
|
||||||
client_states[cid] = payload
|
client_states[cid] = payload
|
||||||
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
|
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
|
||||||
|
|
||||||
@@ -241,7 +254,7 @@ def on_message(client, userdata, msg):
|
|||||||
save_cache(client_telemetry)
|
save_cache(client_telemetry)
|
||||||
|
|
||||||
# --- DEVICE HEALTH MANAGEMENT ---
|
# --- DEVICE HEALTH MANAGEMENT ---
|
||||||
elif parts[0] == 'devices' and len(parts) >= 3 and parts[2] == 'services':
|
elif p0 == 'devices' and len(parts) >= 3 and p2 == 'services':
|
||||||
try:
|
try:
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
device_health[cid] = {
|
device_health[cid] = {
|
||||||
@@ -275,7 +288,7 @@ def on_message(client, userdata, msg):
|
|||||||
logger.error(f"Error parsing health data: {e}")
|
logger.error(f"Error parsing health data: {e}")
|
||||||
|
|
||||||
# --- DMR GATEWAY MANAGEMENT ---
|
# --- 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')):
|
elif len(parts) >= 4 and p0 == 'data' and p2 == 'dmrgateway' and (parts[3].upper().startswith('NETWORK') or parts[3].upper().startswith('DMR NETWORK')):
|
||||||
try:
|
try:
|
||||||
cid = parts[1].lower()
|
cid = parts[1].lower()
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
@@ -301,14 +314,58 @@ def on_message(client, userdata, msg):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error parsing DMRGateway for {cid}: {e}")
|
logger.error(f"Error parsing DMRGateway for {cid}: {e}")
|
||||||
|
|
||||||
|
# --- MMDVMHOST INFO MANAGEMENT (FREQUENZE & LOCATION) ---
|
||||||
|
elif len(parts) >= 4 and p0 == 'data' and p2 == 'mmdvmhost' and parts[3].lower() == 'info':
|
||||||
|
try:
|
||||||
|
cid = parts[1].lower()
|
||||||
|
data = json.loads(payload)
|
||||||
|
|
||||||
|
# Estrazione dati
|
||||||
|
tx = data.get("TXFrequency", "0")
|
||||||
|
rx = data.get("RXFrequency", "0")
|
||||||
|
lat = data.get("Latitude", "0.0")
|
||||||
|
lon = data.get("Longitude", "0.0")
|
||||||
|
loc = data.get("Location", "Sconosciuta")
|
||||||
|
|
||||||
|
def format_freq(f):
|
||||||
|
if str(f).isdigit() and int(f) > 0:
|
||||||
|
return f"{int(f)/1000000:.3f} MHz"
|
||||||
|
return str(f)
|
||||||
|
|
||||||
|
node_info[cid] = {
|
||||||
|
"tx": format_freq(tx),
|
||||||
|
"rx": format_freq(rx),
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"loc": loc
|
||||||
|
}
|
||||||
|
socketio.emit('dati_aggiornati')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing MMDVMHost info for {cid}: {e}")
|
||||||
|
|
||||||
|
# --- MMDVMHOST GENERAL MANAGEMENT (CALLSIGN & ID & DUPLEX) ---
|
||||||
|
elif len(parts) >= 4 and p0 == 'data' and p2 == 'mmdvmhost' and parts[3].lower() == 'general':
|
||||||
|
try:
|
||||||
|
cid = parts[1].lower()
|
||||||
|
data = json.loads(payload)
|
||||||
|
callsign = data.get("Callsign", "")
|
||||||
|
radio_id = data.get("Id", "")
|
||||||
|
duplex = data.get("Duplex", "1") # 1 = Repeater, 0 = Simplex
|
||||||
|
|
||||||
|
if callsign:
|
||||||
|
node_general[cid] = {"callsign": callsign, "radio_id": radio_id, "duplex": str(duplex)}
|
||||||
|
socketio.emit('dati_aggiornati')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing MMDVMHost general for {cid}: {e}")
|
||||||
|
|
||||||
# --- OTHER GATEWAYS MANAGEMENT ---
|
# --- OTHER GATEWAYS MANAGEMENT ---
|
||||||
elif parts[0] in ['dmr-gateway', 'nxdn-gateway', 'ysf-gateway', 'p25-gateway', 'dstar-gateway']:
|
elif p0 in ['dmrgateway', 'nxdngateway', 'ysfgateway', 'p25gateway', 'dstargateway']:
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
proto = "DMR"
|
proto = "DMR"
|
||||||
if "nxdn" in parts[0]: proto = "NXDN"
|
if "nxdn" in p0: proto = "NXDN"
|
||||||
elif "ysf" in parts[0]: proto = "YSF"
|
elif "ysf" in p0: proto = "YSF"
|
||||||
elif "p25" in parts[0]: proto = "P25"
|
elif "p25" in p0: proto = "P25"
|
||||||
elif "dstar" in parts[0]: proto = "D-STAR"
|
elif "dstar" in p0: proto = "D-STAR"
|
||||||
|
|
||||||
m = ""
|
m = ""
|
||||||
if 'status' in data:
|
if 'status' in data:
|
||||||
@@ -430,7 +487,9 @@ def get_states():
|
|||||||
"states": client_states,
|
"states": client_states,
|
||||||
"telemetry": client_telemetry,
|
"telemetry": client_telemetry,
|
||||||
"health": device_health,
|
"health": device_health,
|
||||||
"networks": network_mapping
|
"networks": network_mapping,
|
||||||
|
"info": node_info,
|
||||||
|
"general": node_general
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/stats')
|
@app.route('/api/stats')
|
||||||
|
|||||||
+13
-1
@@ -4,7 +4,19 @@
|
|||||||
"port": 1883,
|
"port": 1883,
|
||||||
"user": "your_username",
|
"user": "your_username",
|
||||||
"password": "your_password",
|
"password": "your_password",
|
||||||
"client_id": "fleet_backend_prod"
|
"client_id": "fleet_backend_prod",
|
||||||
|
"topics": [
|
||||||
|
"servizi/+/stat",
|
||||||
|
"dmr-gateway/+/json",
|
||||||
|
"devices/+/services",
|
||||||
|
"nxdn-gateway/+/json",
|
||||||
|
"ysf-gateway/+/json",
|
||||||
|
"p25-gateway/+/json",
|
||||||
|
"dstar-gateway/+/json",
|
||||||
|
"mmdvm/+/json",
|
||||||
|
"devices/#",
|
||||||
|
"data/#"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"web_admin": {
|
"web_admin": {
|
||||||
"default_user": "admin",
|
"default_user": "admin",
|
||||||
|
|||||||
+22
-8
@@ -2,6 +2,13 @@
|
|||||||
INSTALLATION GUIDE - FLEET CONTROL CONSOLE (SERVER)
|
INSTALLATION GUIDE - FLEET CONTROL CONSOLE (SERVER)
|
||||||
============================================================
|
============================================================
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
0. IMPORTANT: ROOT PRIVILEGES
|
||||||
|
------------------------------------------------------------
|
||||||
|
All installation steps must be executed as the "root" user.
|
||||||
|
Before starting, elevate your privileges by running:
|
||||||
|
sudo su
|
||||||
|
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
1. PRE-REQUISITES (CRITICAL)
|
1. PRE-REQUISITES (CRITICAL)
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
@@ -9,8 +16,8 @@ Before installing Python dependencies, you must install
|
|||||||
system compilers, development libraries, and pip/venv tools.
|
system compilers, development libraries, and pip/venv tools.
|
||||||
|
|
||||||
Debian/Ubuntu:
|
Debian/Ubuntu:
|
||||||
sudo apt update
|
apt update
|
||||||
sudo apt install build-essential python3-dev libssl-dev libffi-dev python3-pip python3-venv
|
apt install build-essential python3-dev libssl-dev libffi-dev python3-pip python3-venv
|
||||||
|
|
||||||
Create and activate a virtual environment (CRITICAL on Debian 12+):
|
Create and activate a virtual environment (CRITICAL on Debian 12+):
|
||||||
cd /opt/fleet-control-server
|
cd /opt/fleet-control-server
|
||||||
@@ -62,14 +69,21 @@ ExecStart=/opt/fleet-control-server/venv/bin/gunicorn -k ...
|
|||||||
Configuration:
|
Configuration:
|
||||||
1. Copy .service file to '/etc/systemd/system/':
|
1. Copy .service file to '/etc/systemd/system/':
|
||||||
sudo cp fleet-console.service /etc/systemd/system/
|
sudo cp fleet-console.service /etc/systemd/system/
|
||||||
2. Reload systemd: sudo systemctl daemon-reload
|
2. Reload systemd: systemctl daemon-reload
|
||||||
3. Enable on boot: sudo systemctl enable fleet-console
|
3. Enable on boot: systemctl enable fleet-console
|
||||||
4. Start service: sudo systemctl start fleet-console
|
4. Start service: systemctl start fleet-console
|
||||||
|
|
||||||
============================================================
|
============================================================
|
||||||
GUIDA ALL'INSTALLAZIONE - SERVER (ITALIANO)
|
GUIDA ALL'INSTALLAZIONE - SERVER (ITALIANO)
|
||||||
============================================================
|
============================================================
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
0. IMPORTANTE: PRIVILEGI DI ROOT
|
||||||
|
------------------------------------------------------------
|
||||||
|
Tutti i passaggi di installazione devono essere eseguiti
|
||||||
|
come utente "root". Prima di iniziare, eleva i tuoi privilegi:
|
||||||
|
sudo su
|
||||||
|
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
1. REQUISITI PRELIMINARI (CRITICI)
|
1. REQUISITI PRELIMINARI (CRITICI)
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
@@ -123,6 +137,6 @@ ExecStart=/opt/fleet-control-server/venv/bin/gunicorn -k ...
|
|||||||
|
|
||||||
Passaggi:
|
Passaggi:
|
||||||
1. Copia il file in systemd: sudo cp fleet-console.service /etc/systemd/system/
|
1. Copia il file in systemd: sudo cp fleet-console.service /etc/systemd/system/
|
||||||
2. Ricarica la configurazione: sudo systemctl daemon-reload
|
2. Ricarica la configurazione: systemctl daemon-reload
|
||||||
3. Abilita all'avvio: sudo systemctl enable fleet-console
|
3. Abilita all'avvio: systemctl enable fleet-console
|
||||||
4. Avvia il servizio: sudo systemctl start fleet-console
|
4. Avvia il servizio: systemctl start fleet-console
|
||||||
|
|||||||
+193
-26
@@ -47,6 +47,37 @@
|
|||||||
/* Telemetria stile terminale */
|
/* Telemetria stile terminale */
|
||||||
.health-bar { display: flex; justify-content: space-between; font-size: 0.75rem; font-weight: 600; background: #010409; padding: 8px 12px; border-radius: 3px; margin-bottom: 15px; border: 1px solid var(--border-color); font-family: 'JetBrains Mono', monospace; }
|
.health-bar { display: flex; justify-content: space-between; font-size: 0.75rem; font-weight: 600; background: #010409; padding: 8px 12px; border-radius: 3px; margin-bottom: 15px; border: 1px solid var(--border-color); font-family: 'JetBrains Mono', monospace; }
|
||||||
|
|
||||||
|
/* Frequenze Radio */
|
||||||
|
.freq-bar { display: flex; justify-content: space-around; font-size: 0.8rem; font-weight: 800; background: rgba(0,0,0,0.2); padding: 8px 10px; border-radius: 3px; margin-bottom: 15px; border: 1px dashed var(--border-color); font-family: 'JetBrains Mono', monospace; }
|
||||||
|
.freq-tx { color: #f87171; } /* Rosso per la TX */
|
||||||
|
.freq-rx { color: #4ade80; } /* Verde per la RX */
|
||||||
|
|
||||||
|
/* Metadati Nodo (Location, Coordinate) */
|
||||||
|
.node-meta { font-size: 0.75rem; color: #8b949e; text-align: center; margin-bottom: 15px; line-height: 1.4; }
|
||||||
|
.node-meta-item { display: block; }
|
||||||
|
|
||||||
|
/* --- Badge Tipo Nodo --- */
|
||||||
|
.type-badge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.type-repeater {
|
||||||
|
background: rgba(47, 129, 247, 0.15);
|
||||||
|
color: var(--primary);
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
.type-hotspot {
|
||||||
|
background: rgba(163, 113, 247, 0.15);
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* Display Stato */
|
/* Display Stato */
|
||||||
.status-display { text-align: center; font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; font-weight: 700; padding: 8px; border-radius: 3px; background: #010409; border: 1px solid var(--border-color); margin-bottom: 15px; color: var(--text-muted); }
|
.status-display { text-align: center; font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; font-weight: 700; padding: 8px; border-radius: 3px; background: #010409; border: 1px solid var(--border-color); margin-bottom: 15px; color: var(--text-muted); }
|
||||||
.card.online .status-display { color: var(--success); border-color: rgba(46, 160, 67, 0.4); }
|
.card.online .status-display { color: var(--success); border-color: rgba(46, 160, 67, 0.4); }
|
||||||
@@ -194,7 +225,7 @@
|
|||||||
<span>Gitea Home</span>
|
<span>Gitea Home</span>
|
||||||
</a>
|
</a>
|
||||||
<span style="opacity: 0.5;">|</span>
|
<span style="opacity: 0.5;">|</span>
|
||||||
<a href="https://github.com/picchiosat/fleet-control-console" target="_blank" style="color: inherit; text-decoration: none; display: flex; align-items: center; gap: 6px; transition: color 0.2s;">
|
<a href="https://github.com/picchiosat/fleet-control-server" target="_blank" style="color: inherit; text-decoration: none; display: flex; align-items: center; gap: 6px; transition: color 0.2s;">
|
||||||
<svg height="18" viewBox="0 0 16 16" width="18" style="fill: currentColor; vertical-align: middle;"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
<svg height="18" viewBox="0 0 16 16" width="18" style="fill: currentColor; vertical-align: middle;"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
||||||
<span>GitHub Mirror</span>
|
<span>GitHub Mirror</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -326,14 +357,15 @@
|
|||||||
<div class="modal-content" style="width:90%; max-width:400px; text-align:center;">
|
<div class="modal-content" style="width:90%; max-width:400px; text-align:center;">
|
||||||
<h2 style="margin-top:0; color:var(--danger);">🚨 <span data-i18n="titleGlobal">GLOBAL OVERRIDE</span> 🚨</h2>
|
<h2 style="margin-top:0; color:var(--danger);">🚨 <span data-i18n="titleGlobal">GLOBAL OVERRIDE</span> 🚨</h2>
|
||||||
<p style="margin-bottom:25px; color:var(--text-muted); font-weight: 600;" id="override-desc">Select the profile to send to the ENTIRE network:</p>
|
<p style="margin-bottom:25px; color:var(--text-muted); font-weight: 600;" id="override-desc">Select the profile to send to the ENTIRE network:</p>
|
||||||
<div style="display:flex; flex-direction:column; gap:15px;">
|
|
||||||
<button id="btn-global-A" onclick="sendGlobalAction('A')" class="btn-cmd" style="background:var(--accent); padding:15px; font-size:1.1rem;">PROFILE A</button>
|
<div id="global-btn-container" style="display:flex; flex-direction:column; gap:15px; margin-bottom: 15px;"></div>
|
||||||
<button id="btn-global-B" onclick="sendGlobalAction('B')" class="btn-cmd" style="background:#eab308; padding:15px; font-size:1.1rem;">PROFILE B</button>
|
|
||||||
<button onclick="document.getElementById('override-modal').style.display='none'" class="btn-cmd" style="background:var(--text-muted); padding:12px; margin-top:10px;" data-i18n="btnCancel">CANCEL</button>
|
<button onclick="document.getElementById('override-modal').style.display='none'" class="btn-cmd" style="background:var(--text-muted); padding:12px; width:100%;" data-i18n="btnCancel">CANCEL</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="reset-hat-modal" class="modal-overlay">
|
<div id="reset-hat-modal" class="modal-overlay">
|
||||||
<div class="modal-content" style="width:90%; max-width:400px; text-align:center; border: 1px solid var(--danger);">
|
<div class="modal-content" style="width:90%; max-width:400px; text-align:center; border: 1px solid var(--danger);">
|
||||||
<h2 style="margin-top:0; color:var(--danger);">🔌 <span data-i18n="btnHat">RESET HAT</span> 🔌</h2>
|
<h2 style="margin-top:0; color:var(--danger);">🔌 <span data-i18n="btnHat">RESET HAT</span> 🔌</h2>
|
||||||
@@ -582,12 +614,22 @@
|
|||||||
let showReboot = (role === 'admin');
|
let showReboot = (role === 'admin');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card" id="card-${c.id}">
|
<div class="card" id="card-${c.id}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="client-name" title="${c.name}">${c.name}</span>
|
<span class="client-name" title="${c.name}">${c.name}</span>
|
||||||
<span class="badge-id">ID: ${c.id.toUpperCase()}</span>
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span class="badge-id">ID: ${c.id.toUpperCase()}</span>
|
||||||
|
<div id="type-badge-${c.id}" class="type-badge" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="freq-bar" id="freq-container-${c.id}" style="display: none;">
|
||||||
|
<span><span class="freq-tx">TX:</span> <span id="freq-tx-${c.id}">...</span></span>
|
||||||
|
<span><span class="freq-rx">RX:</span> <span id="freq-rx-${c.id}">...</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="node-meta" id="meta-container-${c.id}" style="display: none;">
|
||||||
|
<span class="node-meta-item">📍 <strong id="loc-${c.id}">...</strong></span>
|
||||||
|
<span class="node-meta-item">Lat: <span id="lat-${c.id}">...</span> | Lon: <span id="lon-${c.id}">...</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="health-bar" id="health-${c.id}" style="display: none;">
|
<div class="health-bar" id="health-${c.id}" style="display: none;">
|
||||||
<span>⚡ <span id="cpu-${c.id}">--</span>%</span>
|
<span>⚡ <span id="cpu-${c.id}">--</span>%</span>
|
||||||
<span>🌡️ <span id="temp-${c.id}">--</span>°C</span>
|
<span>🌡️ <span id="temp-${c.id}">--</span>°C</span>
|
||||||
@@ -609,19 +651,21 @@
|
|||||||
<span style="font-size: 0.85rem;" data-i18n="warnDaemon">${t('warnDaemon')}</span>
|
<span style="font-size: 0.85rem;" data-i18n="warnDaemon">${t('warnDaemon')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions" style="${(isAuthenticated && canControl) ? 'display:flex;' : 'display:none'}">
|
<div id="prof-btns-${c.id}" style="width: 100%; display: flex; gap: 10px; display: none;"></div>
|
||||||
<button id="btn-profA-${c.id}" class="btn-cmd" style="background: var(--accent);" title="${t('ttProfA')}" onclick="confirmSwitch('${c.id}', 'A')">PROFILE A</button>
|
|
||||||
<button id="btn-profB-${c.id}" class="btn-cmd" style="background: #eab308;" title="${t('ttProfB')}" onclick="confirmSwitch('${c.id}', 'B')">PROFILE B</button>
|
|
||||||
|
|
||||||
<div style="width: 100%; display: flex; gap: 10px;">
|
${canControl ? `
|
||||||
|
<div style="width: 100%; display: flex; gap: 10px; margin-bottom: 8px;">
|
||||||
<button class="btn-cmd" style="background: var(--success);" title="${t('ttTgOn')}" onclick="sendTgCommand('${c.id}', 'TG:ON')">🔔 Telegram ON</button>
|
<button class="btn-cmd" style="background: var(--success);" title="${t('ttTgOn')}" onclick="sendTgCommand('${c.id}', 'TG:ON')">🔔 Telegram ON</button>
|
||||||
<button class="btn-cmd" style="background: var(--text-muted);" title="${t('ttTgOff')}" onclick="sendTgCommand('${c.id}', 'TG:OFF')">🔇 Telegram OFF</button>
|
<button class="btn-cmd" style="background: var(--text-muted);" title="${t('ttTgOff')}" onclick="sendTgCommand('${c.id}', 'TG:OFF')">🔇 Telegram OFF</button>
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
${showReboot ? `
|
${showReboot ? `
|
||||||
<button id="btn-svc-${c.id}" class="btn-cmd" style="background: #334155;" title="${t('ttSvc')}" onclick="openServicesModal('${c.id}')">${t('btnSvc')}</button>
|
<div style="width: 100%; display: flex; gap: 8px;">
|
||||||
<button class="btn-cmd" style="background: #8e44ad;" title="${t('ttFile')}" onclick="openConfigsModal('${c.id}')">${t('btnFile')}</button>
|
<button id="btn-svc-${c.id}" class="btn-cmd" style="background: #334155; padding: 8px 4px;" title="${t('ttSvc')}" onclick="openServicesModal('${c.id}')">${t('btnSvc')}</button>
|
||||||
<button class="btn-cmd" style="background: #ea580c;" title="${t('ttHat')}" onclick="confirmHatReset('${c.id}')">${t('btnHat')}</button>
|
<button class="btn-cmd" style="background: #8e44ad; padding: 8px 4px;" title="${t('ttFile')}" onclick="openConfigsModal('${c.id}')">${t('btnFile')}</button>
|
||||||
<button class="btn-cmd" style="background: var(--danger);" title="${t('ttBoot')}" onclick="confirmReboot('${c.id}')">${t('btnBoot')}</button>
|
<button class="btn-cmd" style="background: #ea580c; padding: 8px 4px;" title="${t('ttHat')}" onclick="confirmHatReset('${c.id}')">${t('btnHat')}</button>
|
||||||
|
<button class="btn-cmd" style="background: var(--danger); padding: 8px 4px;" title="${t('ttBoot')}" onclick="confirmReboot('${c.id}')">${t('btnBoot')}</button>
|
||||||
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -654,10 +698,46 @@
|
|||||||
const statusDiv = document.getElementById(`status-${c.id}`); const cardDiv = document.getElementById(`card-${c.id}`);
|
const statusDiv = document.getElementById(`status-${c.id}`); const cardDiv = document.getElementById(`card-${c.id}`);
|
||||||
if (!statusDiv || !cardDiv) return;
|
if (!statusDiv || !cardDiv) return;
|
||||||
|
|
||||||
|
// 1. PRIMA CALCOLIAMO SE È ONLINE
|
||||||
let stateValue = data.states[c.id.toLowerCase()] || data.states[c.id] || "OFFLINE";
|
let stateValue = data.states[c.id.toLowerCase()] || data.states[c.id] || "OFFLINE";
|
||||||
stateValue = String(stateValue).trim().toUpperCase(); statusDiv.innerText = stateValue;
|
stateValue = String(stateValue).trim().toUpperCase(); statusDiv.innerText = stateValue;
|
||||||
const isOnline = !stateValue.includes("OFF") && stateValue !== "";
|
const isOnline = !stateValue.includes("OFF") && stateValue !== "";
|
||||||
|
|
||||||
|
// 2. POI AGGIORNIAMO NOME E ID DINAMICI
|
||||||
|
let genObj = data.general && data.general[c.id.toLowerCase()];
|
||||||
|
const nameSpan = cardDiv.querySelector('.client-name');
|
||||||
|
const idBadge = cardDiv.querySelector('.badge-id');
|
||||||
|
const typeBadge = document.getElementById(`type-badge-${c.id}`); // <--- Peschiamo il nostro nuovo badge
|
||||||
|
|
||||||
|
if (genObj && isOnline) {
|
||||||
|
nameSpan.innerText = genObj.callsign;
|
||||||
|
idBadge.innerText = `ID: ${genObj.radio_id}`;
|
||||||
|
idBadge.style.background = "var(--primary)";
|
||||||
|
idBadge.style.color = "#ffffff";
|
||||||
|
idBadge.style.borderColor = "var(--primary)";
|
||||||
|
|
||||||
|
// --- LOGICA BADGE HOTSPOT/REPEATER ---
|
||||||
|
if (typeBadge && genObj.duplex !== undefined) {
|
||||||
|
typeBadge.style.display = 'inline-block';
|
||||||
|
if (genObj.duplex === "1") {
|
||||||
|
typeBadge.innerText = "REPEATER";
|
||||||
|
typeBadge.className = "type-badge type-repeater";
|
||||||
|
} else {
|
||||||
|
typeBadge.innerText = "HOTSPOT";
|
||||||
|
typeBadge.className = "type-badge type-hotspot";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nameSpan.innerText = c.name;
|
||||||
|
idBadge.innerText = `ID: ${c.id.toUpperCase()}`;
|
||||||
|
idBadge.style.background = "rgba(255,255,255,0.1)";
|
||||||
|
idBadge.style.color = "var(--text-muted)";
|
||||||
|
idBadge.style.borderColor = "var(--border-color)";
|
||||||
|
|
||||||
|
if (typeBadge) typeBadge.style.display = 'none'; // Nascondilo se il nodo va offline
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. TELEMETRIA E STATO (PULITO DAI DUPLICATI)
|
||||||
let telemetryObj = data.telemetry[c.id.toLowerCase()] || data.telemetry[c.id] || { ts1: "...", ts2: "...", alt: "", idle: true };
|
let telemetryObj = data.telemetry[c.id.toLowerCase()] || data.telemetry[c.id] || { ts1: "...", ts2: "...", alt: "", idle: true };
|
||||||
if (typeof telemetryObj === 'string') telemetryObj = { ts1: telemetryObj, ts2: "...", alt: "", idle: true };
|
if (typeof telemetryObj === 'string') telemetryObj = { ts1: telemetryObj, ts2: "...", alt: "", idle: true };
|
||||||
|
|
||||||
@@ -687,8 +767,17 @@
|
|||||||
let netObj = data.networks && data.networks[c.id.toLowerCase()] ? data.networks[c.id.toLowerCase()] : {ts1: "", ts2: ""};
|
let netObj = data.networks && data.networks[c.id.toLowerCase()] ? data.networks[c.id.toLowerCase()] : {ts1: "", ts2: ""};
|
||||||
activeModeColor = "var(--primary)";
|
activeModeColor = "var(--primary)";
|
||||||
|
|
||||||
if (ts1Div && ts2Div) {
|
if (ts1Div && ts2Div) {
|
||||||
|
// --- LOGICA DUPLEX / SIMPLEX ---
|
||||||
|
let isSimplex = (genObj && genObj.duplex === "0");
|
||||||
|
|
||||||
|
// Nascondiamo il div del TS1 se il nodo è Simplex
|
||||||
|
ts1Div.style.display = isSimplex ? 'none' : 'block';
|
||||||
|
|
||||||
[ts1Div, ts2Div].forEach((div, idx) => {
|
[ts1Div, ts2Div].forEach((div, idx) => {
|
||||||
|
// Se è simplex, saltiamo del tutto l'aggiornamento grafico del TS1
|
||||||
|
if (isSimplex && idx === 0) return;
|
||||||
|
|
||||||
const val = idx === 0 ? telemetryObj.ts1 : telemetryObj.ts2; const netName = idx === 0 ? netObj.ts1 : netObj.ts2;
|
const val = idx === 0 ? telemetryObj.ts1 : telemetryObj.ts2; const netName = idx === 0 ? netObj.ts1 : netObj.ts2;
|
||||||
const fullLabel = netName ? `TS${idx + 1} [${netName}]` : `TS${idx + 1}`;
|
const fullLabel = netName ? `TS${idx + 1} [${netName}]` : `TS${idx + 1}`;
|
||||||
div.innerText = `${fullLabel}: ${val}`;
|
div.innerText = `${fullLabel}: ${val}`;
|
||||||
@@ -701,6 +790,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let healthObj = data.health && data.health[c.id.toLowerCase()];
|
let healthObj = data.health && data.health[c.id.toLowerCase()];
|
||||||
|
let infoObj = data.info && data.info[c.id.toLowerCase()];
|
||||||
|
const freqContainer = document.getElementById(`freq-container-${c.id}`);
|
||||||
|
const freqTx = document.getElementById(`freq-tx-${c.id}`);
|
||||||
|
const freqRx = document.getElementById(`freq-rx-${c.id}`);
|
||||||
|
const metaContainer = document.getElementById(`meta-container-${c.id}`);
|
||||||
|
const locSpan = document.getElementById(`loc-${c.id}`);
|
||||||
|
const latSpan = document.getElementById(`lat-${c.id}`);
|
||||||
|
const lonSpan = document.getElementById(`lon-${c.id}`);
|
||||||
|
|
||||||
|
if (infoObj && isOnline) {
|
||||||
|
if (freqContainer) {
|
||||||
|
freqContainer.style.display = 'flex';
|
||||||
|
freqTx.innerText = infoObj.tx;
|
||||||
|
freqRx.innerText = infoObj.rx;
|
||||||
|
}
|
||||||
|
if (metaContainer) {
|
||||||
|
metaContainer.style.display = 'block';
|
||||||
|
locSpan.innerText = infoObj.loc;
|
||||||
|
latSpan.innerText = infoObj.lat;
|
||||||
|
lonSpan.innerText = infoObj.lon;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (freqContainer) freqContainer.style.display = 'none';
|
||||||
|
if (metaContainer) metaContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
const healthContainer = document.getElementById(`health-${c.id}`); const cpuSpan = document.getElementById(`cpu-${c.id}`); const tempSpan = document.getElementById(`temp-${c.id}`); const ramSpan = document.getElementById(`ram-${c.id}`); const diskSpan = document.getElementById(`disk-${c.id}`);
|
const healthContainer = document.getElementById(`health-${c.id}`); const cpuSpan = document.getElementById(`cpu-${c.id}`); const tempSpan = document.getElementById(`temp-${c.id}`); const ramSpan = document.getElementById(`ram-${c.id}`); const diskSpan = document.getElementById(`disk-${c.id}`);
|
||||||
|
|
||||||
if (healthObj && isOnline) {
|
if (healthObj && isOnline) {
|
||||||
@@ -709,10 +824,28 @@
|
|||||||
let tempVal = healthObj.temp; tempSpan.innerText = tempVal; tempSpan.style.color = tempVal < 55 ? 'var(--success)' : (tempVal < 70 ? '#f59e0b' : 'var(--danger)');
|
let tempVal = healthObj.temp; tempSpan.innerText = tempVal; tempSpan.style.color = tempVal < 55 ? 'var(--success)' : (tempVal < 70 ? '#f59e0b' : 'var(--danger)');
|
||||||
ramSpan.innerText = healthObj.ram; let d = healthObj.disk; diskSpan.innerText = d; diskSpan.style.color = d < 85 ? 'inherit' : (d < 95 ? '#f59e0b' : 'var(--danger)');
|
ramSpan.innerText = healthObj.ram; let d = healthObj.disk; diskSpan.innerText = d; diskSpan.style.color = d < 85 ? 'inherit' : (d < 95 ? '#f59e0b' : 'var(--danger)');
|
||||||
|
|
||||||
let profA = (healthObj.profiles && healthObj.profiles.A) ? healthObj.profiles.A : "PROFILE A"; let profB = (healthObj.profiles && healthObj.profiles.B) ? healthObj.profiles.B : "PROFILE B";
|
const profBtnsDiv = document.getElementById(`prof-btns-${c.id}`);
|
||||||
const btnA = document.getElementById(`btn-profA-${c.id}`); const btnB = document.getElementById(`btn-profB-${c.id}`);
|
const profKeys = JSON.stringify(healthObj.profiles || {});
|
||||||
if (btnA) { btnA.innerText = profA; btnA.title = `${t('ttSwitchTo')}${profA}`; }
|
|
||||||
if (btnB) { btnB.innerText = profB; btnB.title = `${t('ttSwitchTo')}${profB}`; }
|
if (profBtnsDiv && profBtnsDiv.dataset.keys !== profKeys) {
|
||||||
|
profBtnsDiv.dataset.keys = profKeys;
|
||||||
|
if (healthObj.profiles && Object.keys(healthObj.profiles).length > 0) {
|
||||||
|
profBtnsDiv.style.display = 'flex';
|
||||||
|
let html = '';
|
||||||
|
const colors = ["var(--accent)", "#eab308", "var(--success)", "var(--primary)"];
|
||||||
|
let idx = 0;
|
||||||
|
for (const [pKey, pLabel] of Object.entries(healthObj.profiles)) {
|
||||||
|
let col = colors[idx % colors.length];
|
||||||
|
html += `<button class="btn-cmd" style="background: ${col}; flex: 1;" title="${t('ttSwitchTo')}${pLabel}" onclick="confirmSwitch('${c.id}', '${pKey}')">${pLabel}</button>`;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
profBtnsDiv.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
profBtnsDiv.style.display = 'none';
|
||||||
|
profBtnsDiv.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else { if (healthContainer) healthContainer.style.display = 'none'; }
|
} else { if (healthContainer) healthContainer.style.display = 'none'; }
|
||||||
|
|
||||||
globalHealthData[c.id.toLowerCase()] = healthObj;
|
globalHealthData[c.id.toLowerCase()] = healthObj;
|
||||||
@@ -731,7 +864,16 @@
|
|||||||
|
|
||||||
if (isOnline) {
|
if (isOnline) {
|
||||||
cardDiv.classList.add('online'); statusDiv.classList.remove('status-offline'); cardDiv.style.opacity = "1"; cardDiv.style.filter = "none";
|
cardDiv.classList.add('online'); statusDiv.classList.remove('status-offline'); cardDiv.style.opacity = "1"; cardDiv.style.filter = "none";
|
||||||
cardDiv.style.setProperty('border-top-color', (!isTx && isIdle) ? "var(--border-color)" : activeModeColor, 'important');
|
|
||||||
|
// --- INIZIO LOGICA SMART HANG-TIME ---
|
||||||
|
if (isTx) cardDiv.dataset.lastTx = Date.now(); // Memorizza quando è avvenuto l'ultimo TX
|
||||||
|
let timeSinceTx = Date.now() - (parseInt(cardDiv.dataset.lastTx) || 0);
|
||||||
|
|
||||||
|
// Accendi se in TX, OPPURE se in Hang Time (ma spegni a forza dopo 30 secondi di inattività)
|
||||||
|
let showActiveColor = isTx || (!isIdle && timeSinceTx < 180000);
|
||||||
|
|
||||||
|
cardDiv.style.setProperty('border-top-color', showActiveColor ? activeModeColor : "var(--border-color)", 'important');
|
||||||
|
// --- FINE LOGICA ---
|
||||||
if(isTx) {
|
if(isTx) {
|
||||||
let shadowRGB = '59, 130, 246';
|
let shadowRGB = '59, 130, 246';
|
||||||
if (activeModeColor === '#10b981') shadowRGB = '16, 185, 129';
|
if (activeModeColor === '#10b981') shadowRGB = '16, 185, 129';
|
||||||
@@ -829,9 +971,34 @@
|
|||||||
|
|
||||||
// --- SETTINGS ---
|
// --- SETTINGS ---
|
||||||
function triggerGlobalEmergency() {
|
function triggerGlobalEmergency() {
|
||||||
let nameA = "PROFILE A"; let nameB = "PROFILE B";
|
let uniqueProfiles = {};
|
||||||
for (const id in globalHealthData) { if (globalHealthData[id] && globalHealthData[id].profiles) { nameA = globalHealthData[id].profiles.A || nameA; nameB = globalHealthData[id].profiles.B || nameB; break; } }
|
// Raccoglie tutti i profili unici trovati nei nodi online
|
||||||
document.getElementById('btn-global-A').innerText = nameA; document.getElementById('btn-global-B').innerText = nameB; document.getElementById('override-desc').innerText = t('msgOvrSel'); document.getElementById('override-modal').style.display = 'flex';
|
for (const id in globalHealthData) {
|
||||||
|
if (globalHealthData[id] && globalHealthData[id].profiles) {
|
||||||
|
for (const [k, v] of Object.entries(globalHealthData[id].profiles)) {
|
||||||
|
uniqueProfiles[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.getElementById('global-btn-container');
|
||||||
|
let html = '';
|
||||||
|
const colors = ["var(--accent)", "#eab308", "var(--success)", "var(--primary)"];
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(uniqueProfiles)) {
|
||||||
|
let col = colors[idx % colors.length];
|
||||||
|
html += `<button onclick="sendGlobalAction('${k}')" class="btn-cmd" style="background:${col}; padding:15px; font-size:1.1rem; font-weight:bold;">${v}</button>`;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(uniqueProfiles).length === 0) {
|
||||||
|
html = `<p style="color:var(--danger); font-weight:bold;">No profiles configured on the network.</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
document.getElementById('override-desc').innerText = t('msgOvrSel');
|
||||||
|
document.getElementById('override-modal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendGlobalAction(action) {
|
function sendGlobalAction(action) {
|
||||||
|
|||||||
Reference in New Issue
Block a user