Compare commits
4 Commits
080a6af776
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb697750b7 | |||
| df8ac4ab31 | |||
| 1780a4a737 | |||
| 728233998b |
+94
-71
@@ -15,7 +15,7 @@ import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# ==========================================
|
||||
# 0. CONFIGURAZIONE LOGGING & HARDWARE
|
||||
# 0. LOGGING & HARDWARE CONFIGURATION
|
||||
# ==========================================
|
||||
logging.basicConfig(
|
||||
handlers=[
|
||||
@@ -33,48 +33,48 @@ try:
|
||||
GPIO_AVAILABLE = True
|
||||
except ImportError:
|
||||
GPIO_AVAILABLE = False
|
||||
logger.warning("Libreria RPi.GPIO non trovata. Reset hardware disabilitato.")
|
||||
logger.warning("RPi.GPIO library not found. Hardware reset disabled.")
|
||||
|
||||
# ==========================================
|
||||
# 1. CARICAMENTO CONFIGURAZIONE UNIFICATA
|
||||
# 1. UNIFIED CONFIGURATION LOADING
|
||||
# ==========================================
|
||||
CONFIG_PATH = Path("/opt/node_config.json")
|
||||
|
||||
def load_config():
|
||||
try:
|
||||
if not CONFIG_PATH.exists():
|
||||
logger.error(f"ERRORE: File {CONFIG_PATH} non trovato!")
|
||||
logger.error(f"ERROR: File {CONFIG_PATH} not found!")
|
||||
sys.exit(1)
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"ERRORE CRITICO JSON: {e}")
|
||||
logger.error(f"CRITICAL JSON ERROR: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = load_config()
|
||||
|
||||
# Identificativi e Topic
|
||||
# Identifiers and Topics
|
||||
CLIENT_ID = cfg.get('client_id', 'iv3jdv').lower()
|
||||
BASE_TOPIC = cfg.get('mqtt', {}).get('base_topic', f"servizi/{CLIENT_ID}")
|
||||
|
||||
TOPIC_CMD = f"{BASE_TOPIC}/cmnd"
|
||||
TOPIC_STAT = f"{BASE_TOPIC}/stat"
|
||||
|
||||
# Variabili di Stato Globali
|
||||
# Global Status Variables
|
||||
boot_recovered = False
|
||||
current_status = "ONLINE - Pronto"
|
||||
current_status = "ONLINE"
|
||||
auto_healing_counter = {}
|
||||
|
||||
# ==========================================
|
||||
# 2. FUNZIONE NOTIFICA TELEGRAM
|
||||
# 2. TELEGRAM NOTIFICATION FUNCTION
|
||||
# ==========================================
|
||||
def send_telegram_message(message):
|
||||
t_cfg = cfg.get('telegram', {})
|
||||
if not t_cfg.get('enabled', False): return
|
||||
|
||||
ora_attuale = int(time.strftime("%H"))
|
||||
if ora_attuale >= 23 or ora_attuale < 7:
|
||||
logger.info(f"🌙 Notte fonda ({ora_attuale}:00): Notifica Telegram evitata.")
|
||||
current_hour = int(time.strftime("%H"))
|
||||
if current_hour >= 23 or current_hour < 7:
|
||||
logger.info(f"🌙 Late night ({current_hour}:00): Telegram notification skipped.")
|
||||
return
|
||||
|
||||
token = t_cfg.get('token')
|
||||
@@ -87,25 +87,35 @@ def send_telegram_message(message):
|
||||
payload = {"chat_id": chat_id, "text": f"[{CLIENT_ID.upper()}]\n{clean_msg}"}
|
||||
requests.post(url, json=payload, timeout=10)
|
||||
except Exception as e:
|
||||
logger.error(f"Errore invio Telegram: {e}")
|
||||
logger.error(f"Telegram sending error: {e}")
|
||||
|
||||
# ==========================================
|
||||
# 3. LOGICA CAMBIO PROFILO MULTIPLO
|
||||
# 3. MULTIPLE PROFILE SWITCH LOGIC
|
||||
# ==========================================
|
||||
def get_actual_config_from_disk():
|
||||
return "ONLINE - Da memoria"
|
||||
try:
|
||||
path = "/opt/last_profile.txt"
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
label = f.read().strip()
|
||||
if label:
|
||||
return f"ONLINE - {label}"
|
||||
except Exception as e:
|
||||
logger.error(f"Errore lettura memoria profilo: {e}")
|
||||
|
||||
return "ONLINE" # Default se il file non esiste o è vuoto
|
||||
|
||||
def switch_config(config_type):
|
||||
profile = cfg.get('profiles', {}).get(config_type)
|
||||
|
||||
if not profile:
|
||||
return f"ERRORE: Profilo {config_type} non trovato in JSON"
|
||||
return f"ERROR: Profile {config_type} not found in JSON"
|
||||
|
||||
label = profile.get('label', f"Profilo {config_type}")
|
||||
label = profile.get('label', f"Profile {config_type}")
|
||||
services = profile.get('services', [])
|
||||
|
||||
if not services:
|
||||
return f"ERRORE: Nessun servizio configurato per {config_type}"
|
||||
return f"ERROR: No services configured for {config_type}"
|
||||
|
||||
try:
|
||||
for s in services:
|
||||
@@ -113,28 +123,32 @@ def switch_config(config_type):
|
||||
|
||||
for s in services:
|
||||
if not os.path.exists(s['source']):
|
||||
return f"ERRORE: Manca il file sorgente {s['source']}"
|
||||
return f"ERROR: Missing source file {s['source']}"
|
||||
shutil.copy(s['source'], s['target'])
|
||||
|
||||
for s in services:
|
||||
subprocess.run(["sudo", "systemctl", "start", s['name']], check=False)
|
||||
|
||||
send_telegram_message(f"✅ Switch multiplo completato: {label}")
|
||||
# Save the current profile to disk to remember it on reboot
|
||||
with open("/opt/last_profile.txt", "w") as f:
|
||||
f.write(label)
|
||||
|
||||
send_telegram_message(f"✅ Multiple switch completed: {label}")
|
||||
return f"ONLINE - {label}"
|
||||
|
||||
except Exception as e:
|
||||
return f"ERRORE: {str(e)}"
|
||||
return f"ERROR: {str(e)}"
|
||||
|
||||
def force_online_if_needed(client):
|
||||
global boot_recovered, current_status
|
||||
if not boot_recovered:
|
||||
logger.info("⚠️ Recupero memoria saltato. Imposto stato da disco...")
|
||||
logger.info("⚠️ Memory recovery skipped. Setting status from disk...")
|
||||
current_status = get_actual_config_from_disk()
|
||||
client.publish(TOPIC_STAT, current_status, retain=True)
|
||||
boot_recovered = True
|
||||
|
||||
# ==========================================
|
||||
# 4. TELEMETRIA E AUTO-HEALING
|
||||
# 4. TELEMETRY AND AUTO-HEALING
|
||||
# ==========================================
|
||||
def get_cpu_temperature():
|
||||
temp = 0.0
|
||||
@@ -157,8 +171,8 @@ def get_system_status():
|
||||
"processes": {},
|
||||
"timestamp": time.strftime("%H:%M:%S"),
|
||||
"profiles": {
|
||||
"A": cfg.get('profiles', {}).get('A', {}).get('label', 'PROFILO A'),
|
||||
"B": cfg.get('profiles', {}).get('B', {}).get('label', 'PROFILO B')
|
||||
"A": cfg.get('profiles', {}).get('A', {}).get('label', 'PROFILE A'),
|
||||
"B": cfg.get('profiles', {}).get('B', {}).get('label', 'PROFILE B')
|
||||
}
|
||||
}
|
||||
proc_path = Path(cfg['paths'].get('process_list', ''))
|
||||
@@ -169,7 +183,7 @@ def get_system_status():
|
||||
for name in target_processes:
|
||||
name = name.strip().lower()
|
||||
if name: status["processes"][name] = "online" if name in running_names else "offline"
|
||||
except Exception as e: logger.error(f"Errore controllo processi: {e}")
|
||||
except Exception as e: logger.error(f"Process check error: {e}")
|
||||
return status
|
||||
|
||||
def check_auto_healing(client, status):
|
||||
@@ -179,33 +193,33 @@ def check_auto_healing(client, status):
|
||||
attempts = auto_healing_counter.get(proc_name, 0)
|
||||
if attempts < 3:
|
||||
auto_healing_counter[proc_name] = attempts + 1
|
||||
msg = f"🛠 Auto-healing: {proc_name} offline. Riavvio {attempts+1}/3..."
|
||||
msg = f"🛠 Auto-healing: {proc_name} offline. Restarting {attempts+1}/3..."
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", msg)
|
||||
send_telegram_message(msg)
|
||||
|
||||
# --- INIZIO MODIFICA: RESET HARDWARE SPECIFICO PER MMDVMHOST ---
|
||||
# --- START MODIFICATION: SPECIFIC HARDWARE RESET FOR MMDVMHOST ---
|
||||
if proc_name.lower() == "mmdvmhost" and GPIO_AVAILABLE:
|
||||
logger.info("Esecuzione RESET HAT automatico pre-riavvio MMDVMHost...")
|
||||
logger.info("Executing automatic HAT RESET before restarting MMDVMHost...")
|
||||
try:
|
||||
RESET_PIN = 21 # Assicurati che il PIN sia quello corretto per i tuoi nodi
|
||||
RESET_PIN = 21 # Ensure the PIN is correct for your nodes
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(RESET_PIN, GPIO.OUT)
|
||||
# Impulso LOW per resettare
|
||||
# LOW pulse to reset
|
||||
GPIO.output(RESET_PIN, GPIO.LOW)
|
||||
time.sleep(0.5)
|
||||
GPIO.output(RESET_PIN, GPIO.HIGH)
|
||||
GPIO.cleanup(RESET_PIN)
|
||||
# Diamo tempo al microcontrollore di riavviarsi
|
||||
# Give the microcontroller time to restart
|
||||
time.sleep(1.5)
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", "🔌 Impulso GPIO (Reset MMDVM) inviato!")
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", "🔌 GPIO Pulse (MMDVM Reset) sent!")
|
||||
except Exception as e:
|
||||
logger.error(f"Errore GPIO in auto-healing: {e}")
|
||||
# --- FINE MODIFICA ---
|
||||
logger.error(f"GPIO error in auto-healing: {e}")
|
||||
# --- END MODIFICATION ---
|
||||
|
||||
subprocess.run(["sudo", "systemctl", "restart", proc_name])
|
||||
elif attempts == 3:
|
||||
msg = f"🚨 CRITICO: {proc_name} fallito!"
|
||||
msg = f"🚨 CRITICAL: {proc_name} failed!"
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", msg)
|
||||
send_telegram_message(msg)
|
||||
auto_healing_counter[proc_name] = 4
|
||||
@@ -221,9 +235,9 @@ def publish_all(client):
|
||||
if file_list_path.exists():
|
||||
try:
|
||||
files = file_list_path.read_text(encoding="utf-8").splitlines()
|
||||
nomi_estrattti = [Path(f.strip()).stem for f in files if f.strip()]
|
||||
status["config_files"] = nomi_estrattti
|
||||
status["files"] = nomi_estrattti
|
||||
extracted_names = [Path(f.strip()).stem for f in files if f.strip()]
|
||||
status["config_files"] = extracted_names
|
||||
status["files"] = extracted_names
|
||||
except: pass
|
||||
|
||||
client.publish(f"devices/{CLIENT_ID}/services", json.dumps(status), qos=1)
|
||||
@@ -246,7 +260,7 @@ def publish_all_ini_files(client):
|
||||
with open(file_list_path, 'r') as f:
|
||||
files_to_parse = [line.strip() for line in f if line.strip()]
|
||||
except Exception as e:
|
||||
logger.error(f"Errore lettura {file_list_path}: {e}")
|
||||
logger.error(f"Error reading {file_list_path}: {e}")
|
||||
return
|
||||
|
||||
for file_path in files_to_parse:
|
||||
@@ -275,7 +289,7 @@ def publish_all_ini_files(client):
|
||||
topic = f"data/{CLIENT_ID}/{base_name}/{section}"
|
||||
client.publish(topic, json.dumps(payload), retain=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Errore parsing INI per {file_path}: {e}")
|
||||
logger.error(f"Error parsing INI for {file_path}: {e}")
|
||||
|
||||
def write_config_from_json(slug, json_payload):
|
||||
file_list_path = Path(cfg['paths'].get('file_list', ''))
|
||||
@@ -285,21 +299,21 @@ def write_config_from_json(slug, json_payload):
|
||||
for f in files:
|
||||
p = Path(f.strip())
|
||||
if p.stem.lower() == slug.lower():
|
||||
nuovi_dati = json.loads(json_payload)
|
||||
new_data = json.loads(json_payload)
|
||||
shutil.copy(p, str(p) + ".bak")
|
||||
with open(p, 'w', encoding="utf-8") as file: file.write(nuovi_dati.get("raw_text", ""))
|
||||
with open(p, 'w', encoding="utf-8") as file: file.write(new_data.get("raw_text", ""))
|
||||
os.system(f"sudo systemctl restart {slug}")
|
||||
send_telegram_message(f"📝 Config {slug.upper()} aggiornata via Web.")
|
||||
logger.info(f"Configurazione {slug} aggiornata con successo.")
|
||||
send_telegram_message(f"📝 Config {slug.upper()} updated via Web.")
|
||||
logger.info(f"Configuration {slug} updated successfully.")
|
||||
break
|
||||
except Exception as e: logger.error(f"Errore scrittura config: {e}")
|
||||
except Exception as e: logger.error(f"Config writing error: {e}")
|
||||
|
||||
# ==========================================
|
||||
# 5. CALLBACK MQTT
|
||||
# 5. MQTT CALLBACKS
|
||||
# ==========================================
|
||||
def on_connect(client, userdata, flags, reason_code, properties=None):
|
||||
if reason_code == 0:
|
||||
logger.info(f"✅ Connesso al broker MQTT: {CLIENT_ID.upper()}")
|
||||
logger.info(f"✅ Connected to MQTT broker: {CLIENT_ID.upper()}")
|
||||
client.subscribe([(TOPIC_CMD, 0), (TOPIC_STAT, 0)])
|
||||
client.subscribe([
|
||||
("devices/control/request", 0),
|
||||
@@ -310,12 +324,11 @@ def on_connect(client, userdata, flags, reason_code, properties=None):
|
||||
publish_all(client)
|
||||
publish_all_ini_files(client)
|
||||
else:
|
||||
logger.error(f"❌ Errore connessione MQTT. Codice: {reason_code}")
|
||||
logger.error(f"❌ MQTT connection error. Code: {reason_code}")
|
||||
|
||||
def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None):
|
||||
logger.warning(f"⚠️ Disconnessione dal broker MQTT! Codice: {reason_code}")
|
||||
logger.error("Forzo il riavvio del processo per ripristinare la rete in modo pulito...")
|
||||
os._exit(1) # Uccide lo script immediatamente (Systemd lo farà risorgere)
|
||||
logger.warning(f"⚠️ Disconnected from MQTT broker! Code: {reason_code}")
|
||||
logger.info("Waiting for network return. Paho-MQTT will attempt automatic reconnection...")
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
global boot_recovered, current_status, cfg
|
||||
@@ -323,7 +336,7 @@ def on_message(client, userdata, msg):
|
||||
topic = msg.topic
|
||||
|
||||
if topic == TOPIC_STAT and not boot_recovered:
|
||||
if not any(x in payload.upper() for x in ["OFFLINE", "ERRORE", "RIAVVIO"]):
|
||||
if not any(x in payload.upper() for x in ["OFFLINE", "ERROR", "REBOOT"]):
|
||||
current_status = payload
|
||||
boot_recovered = True
|
||||
client.publish(TOPIC_STAT, current_status, retain=True)
|
||||
@@ -336,8 +349,8 @@ def on_message(client, userdata, msg):
|
||||
boot_recovered = True
|
||||
publish_all(client)
|
||||
elif cmd == "REBOOT":
|
||||
client.publish(TOPIC_STAT, f"OFFLINE - Riavvio {CLIENT_ID.upper()}...", retain=False)
|
||||
logger.info("Comando REBOOT ricevuto. Riavvio sistema...")
|
||||
client.publish(TOPIC_STAT, f"OFFLINE - Rebooting {CLIENT_ID.upper()}...", retain=False)
|
||||
logger.info("REBOOT command received. Rebooting system...")
|
||||
time.sleep(1)
|
||||
subprocess.run(["sudo", "reboot"], check=True)
|
||||
elif cmd == 'RESET_HAT':
|
||||
@@ -351,28 +364,31 @@ def on_message(client, userdata, msg):
|
||||
time.sleep(0.5)
|
||||
GPIO.output(RESET_PIN, GPIO.HIGH)
|
||||
GPIO.cleanup(RESET_PIN)
|
||||
logger.info(f"Impulso di RESET inviato al GPIO {RESET_PIN}")
|
||||
logger.info(f"RESET pulse sent to GPIO {RESET_PIN}")
|
||||
time.sleep(1.5)
|
||||
logger.info("Riavvio di MMDVMHost in corso...")
|
||||
logger.info("Restarting MMDVMHost...")
|
||||
subprocess.run(["sudo", "systemctl", "restart", "mmdvmhost"], check=False)
|
||||
client.publish(f"fleet/{CLIENT_ID}/status", "HAT RESET + MMDVM RESTART OK")
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", "🔌 HAT Reset + MMDVMHost Restarted")
|
||||
except Exception as e:
|
||||
logger.error(f"Errore durante il reset GPIO/MMDVMHost: {e}")
|
||||
client.publish(f"fleet/{CLIENT_ID}/status", f"ERRORE RESET: {e}")
|
||||
logger.error(f"Error during GPIO/MMDVMHost reset: {e}")
|
||||
client.publish(f"fleet/{CLIENT_ID}/status", f"RESET ERROR: {e}")
|
||||
|
||||
elif cmd in ["TG:OFF", "TG:ON"]:
|
||||
nuovo_stato = (cmd == "TG:ON")
|
||||
cfg['telegram']['enabled'] = nuovo_stato
|
||||
new_state = (cmd == "TG:ON")
|
||||
cfg['telegram']['enabled'] = new_state
|
||||
try:
|
||||
with open(CONFIG_PATH, 'w') as f: json.dump(cfg, f, indent=4)
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", f"{'🔔' if nuovo_stato else '🔇'} Notifiche {'ON' if nuovo_stato else 'OFF'}")
|
||||
if nuovo_stato: send_telegram_message("Notifiche riattivate!")
|
||||
except Exception as e: logger.error(f"Errore salvataggio stato Telegram: {e}")
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", f"{'🔔' if new_state else '🔇'} Notifications {'ON' if new_state else 'OFF'}")
|
||||
if new_state: send_telegram_message("Notifications reactivated!")
|
||||
except Exception as e: logger.error(f"Error saving Telegram status: {e}")
|
||||
|
||||
elif topic == "devices/control/request" and payload.lower() in ["status", "update"]:
|
||||
logger.info("📥 Received global update command (REQ CONFIG)")
|
||||
publish_all(client)
|
||||
publish_all_ini_files(client)
|
||||
# Force the visual update of the card on the dashboard!
|
||||
client.publish(TOPIC_STAT, current_status, retain=True)
|
||||
|
||||
elif topic == f"devices/{CLIENT_ID}/control":
|
||||
if ":" in payload:
|
||||
@@ -381,11 +397,11 @@ def on_message(client, userdata, msg):
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", action.lower(), service.lower()], check=True)
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", f"✅ {action.upper()}: {service}")
|
||||
logger.info(f"Comando servizio eseguito: {action.upper()} {service}")
|
||||
logger.info(f"Service command executed: {action.upper()} {service}")
|
||||
publish_all(client)
|
||||
except Exception as e:
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", f"❌ ERROR: {str(e)}")
|
||||
logger.error(f"Errore esecuzione comando servizio: {e}")
|
||||
logger.error(f"Error executing service command: {e}")
|
||||
|
||||
elif topic.startswith(f"devices/{CLIENT_ID}/config_set/"):
|
||||
slug = topic.split("/")[-1]
|
||||
@@ -402,6 +418,8 @@ def auto_publish_task(client):
|
||||
time.sleep(cfg['settings'].get('update_interval', 30))
|
||||
|
||||
def start_service():
|
||||
global current_status
|
||||
current_status = get_actual_config_from_disk()
|
||||
client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID.upper())
|
||||
client.will_set(TOPIC_STAT, payload=f"OFFLINE - {CLIENT_ID.upper()}", qos=1, retain=False)
|
||||
client.username_pw_set(cfg['mqtt']['user'], cfg['mqtt']['password'])
|
||||
@@ -409,19 +427,24 @@ def start_service():
|
||||
client.on_disconnect = on_disconnect
|
||||
client.on_message = on_message
|
||||
|
||||
# 1. Start the telemetry "engine" ONLY ONCE
|
||||
threading.Thread(target=auto_publish_task, args=(client,), daemon=True).start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
logger.info("Tentativo di connessione al broker MQTT...")
|
||||
logger.info("Attempting connection to MQTT broker...")
|
||||
client.connect(cfg['mqtt']['broker'], cfg['mqtt']['port'], 60)
|
||||
client.loop_start()
|
||||
threading.Thread(target=auto_publish_task, args=(client,), daemon=True).start()
|
||||
|
||||
# Mantiene il processo vivo
|
||||
# 2. Start network manager in background (handles reconnections automatically!)
|
||||
client.loop_start()
|
||||
|
||||
# 3. Pause main thread indefinitely
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Impossibile connettersi o connessione persa ({e}). Riprovo in 10 secondi...")
|
||||
# This triggers ONLY if the broker is down when the node boots
|
||||
logger.error(f"Broker unreachable at boot ({e}). Retrying in 10 seconds...")
|
||||
time.sleep(10)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -459,6 +459,14 @@ def update_nodes():
|
||||
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
|
||||
|
||||
+102
-137
@@ -12,147 +12,111 @@
|
||||
<link rel="apple-touch-icon" href="/icon-512.png">
|
||||
<meta name="apple-mobile-web-app-title" content="Fleet C2">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
<style>
|
||||
/* --- TEMA NOC (Network Operations Center) --- */
|
||||
:root {
|
||||
--primary: #3b82f6; --success: #10b981; --danger: #ef4444; --accent: #8b5cf6;
|
||||
--bg-gradient: linear-gradient(135deg, #e0e7ff 0%, #ede9fe 100%);
|
||||
--card-bg: rgba(255, 255, 255, 0.6);
|
||||
--border-color: rgba(255, 255, 255, 0.8);
|
||||
--text-main: #1e293b; --text-muted: #64748b;
|
||||
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
|
||||
--topbar-bg: rgba(255, 255, 255, 0.8);
|
||||
--primary: #2f81f7; --success: #2ea043; --danger: #da3633; --accent: #a371f7;
|
||||
--bg-gradient: #0d1117; /* Sfondo nero/grigio profondissimo */
|
||||
--card-bg: #161b22; /* Sfondo grigio antracite per i pannelli */
|
||||
--border-color: #30363d;/* Bordi netti e sottili */
|
||||
--text-main: #c9d1d9; /* Bianco attenuato anti-affaticamento */
|
||||
--text-muted: #8b949e; /* Grigio tecnico per label */
|
||||
--topbar-bg: #161b22;
|
||||
}
|
||||
|
||||
/* Modalità scura "pura" se il toggle viene premuto */
|
||||
body.dark-mode {
|
||||
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #020617 100%);
|
||||
--card-bg: rgba(30, 41, 59, 0.5);
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
--text-main: #f8fafc; --text-muted: #94a3b8;
|
||||
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||
--topbar-bg: rgba(15, 23, 42, 0.8);
|
||||
--bg-gradient: #010409; --card-bg: #0d1117; --border-color: #21262d;
|
||||
--text-main: #f0f6fc; --topbar-bg: #0d1117;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: 'Inter', sans-serif; background: var(--bg-gradient); background-attachment: fixed; margin: 0; padding: 0; color: var(--text-main); transition: color 0.3s; min-height: 100vh; }
|
||||
body { font-family: 'Inter', sans-serif; background: var(--bg-gradient); margin: 0; padding: 0; color: var(--text-main); transition: background-color 0.3s; min-height: 100vh; }
|
||||
|
||||
/* Floating Top Bar */
|
||||
/* Barra superiore squadrata e tecnica */
|
||||
#top-bar-container { position: sticky; top: 15px; z-index: 100; padding: 0 20px; display: flex; justify-content: center; }
|
||||
#top-bar { background: var(--topbar-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); padding: 12px 30px; display: flex; justify-content: space-between; align-items: center; border-radius: 50px; box-shadow: var(--glass-shadow); border: 1px solid var(--border-color); width: 100%; max-width: 1400px; }
|
||||
#top-bar { background: var(--topbar-bg); padding: 12px 30px; display: flex; justify-content: space-between; align-items: center; border-radius: 6px; border: 1px solid var(--border-color); width: 100%; max-width: 1400px; }
|
||||
.title-brand { font-size: 1.2rem; font-weight: 800; letter-spacing: 2px; color: var(--text-main); font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
.title-brand { font-size: 1.3rem; font-weight: 800; letter-spacing: 1px; background: linear-gradient(to right, var(--primary), var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.theme-switch { background: transparent; border: 1px solid var(--border-color); color: var(--text-muted); padding: 6px 14px; border-radius: 4px; cursor: pointer; font-weight: 600; font-size: 0.85rem; font-family: 'JetBrains Mono', monospace; }
|
||||
.theme-switch:hover { color: var(--text-main); border-color: var(--text-main); }
|
||||
|
||||
.theme-switch { background: rgba(128, 128, 128, 0.1); border: 1px solid var(--border-color); color: var(--text-main); padding: 8px 18px; border-radius: 20px; cursor: pointer; font-weight: 600; transition: all 0.2s ease; font-size: 0.85rem; }
|
||||
.theme-switch:hover { background: rgba(128, 128, 128, 0.2); transform: translateY(-1px); }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 20px; max-width: 1400px; margin: 30px auto; padding: 0 20px; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 30px; max-width: 1400px; margin: 40px auto; padding: 0 20px; }
|
||||
/* Pannelli Ripetitori - Zero ombre, bordi netti */
|
||||
.card { background: var(--card-bg) !important; border-radius: 4px; padding: 20px; border: 1px solid var(--border-color); border-top: 4px solid var(--border-color) !important; display: flex; flex-direction: column; box-shadow: none !important; filter: none !important; opacity: 1 !important; }
|
||||
.card.online { border-top-color: var(--success) !important; }
|
||||
|
||||
/* Glass Cards */
|
||||
.card { background: var(--card-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-radius: 20px; padding: 25px; box-shadow: var(--glass-shadow); border: 1px solid var(--border-color); border-top: 5px solid #64748b; transition: transform 0.3s ease, box-shadow 0.3s ease; display: flex; flex-direction: column; }
|
||||
.card:hover { transform: translateY(-4px); box-shadow: 0 12px 40px 0 rgba(0,0,0,0.15); }
|
||||
.card.online { border-top-color: var(--success); }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
|
||||
.client-name { font-size: 1.1rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
|
||||
.badge-id { font-size: 0.75rem; font-family: 'JetBrains Mono', monospace; background: #010409; padding: 4px 8px; border-radius: 3px; border: 1px solid var(--border-color); color: var(--text-muted); }
|
||||
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.client-name { font-size: 1.25rem; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
|
||||
.badge-id { font-size: 0.7rem; font-weight: 800; background: rgba(128, 128, 128, 0.15); padding: 4px 10px; border-radius: 12px; letter-spacing: 0.5px; border: 1px solid var(--border-color); }
|
||||
/* 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: rgba(0,0,0,0.05); padding: 8px 12px; border-radius: 12px; margin-bottom: 15px; border: 1px solid rgba(128,128,128,0.1); }
|
||||
body.dark-mode .health-bar { background: rgba(0,0,0,0.2); }
|
||||
/* 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); }
|
||||
.card.online .status-display { color: var(--success); border-color: rgba(46, 160, 67, 0.4); }
|
||||
.status-offline { color: var(--danger) !important; border-color: rgba(218, 54, 51, 0.4) !important; }
|
||||
|
||||
.status-display { text-align: center; font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; font-weight: 700; padding: 10px; border-radius: 12px; background: rgba(0,0,0,0.05); border: 1px solid var(--border-color); margin-bottom: 15px; transition: all 0.3s; }
|
||||
body.dark-mode .status-display { background: rgba(0,0,0,0.2); color: #10b981; }
|
||||
.status-offline { color: var(--danger) !important; opacity: 0.8; }
|
||||
/* TimeSlots Override */
|
||||
.ts-container { display: flex; flex-direction: column; gap: 6px; margin-bottom: 15px; }
|
||||
.dmr-info { font-size: 0.85rem; font-weight: 600; font-family: 'JetBrains Mono', monospace; padding: 8px 12px; border-radius: 3px; background: #010409 !important; border: 1px solid var(--border-color); border-left: 4px solid var(--border-color) !important; color: var(--text-muted) !important; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
/* TimeSlots */
|
||||
.ts-container { display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px; }
|
||||
.dmr-info { font-size: 0.85rem; font-weight: 600; padding: 10px 15px; border-radius: 12px; background: rgba(59, 130, 246, 0.1); border-left: 4px solid var(--primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: all 0.2s; }
|
||||
/* Terminal Log interno ai pannelli */
|
||||
.terminal-log { background: #010409; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; border-radius: 3px; padding: 10px; height: 130px; overflow-y: auto; border: 1px solid var(--border-color); line-height: 1.4; margin-bottom: 15px; box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); }
|
||||
|
||||
/* Terminal Log */
|
||||
.terminal-log { background: #0f172a; color: #10b981; font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; border-radius: 12px; padding: 12px; height: 130px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.1); line-height: 1.5; margin-bottom: 15px; box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); }
|
||||
/* Bottoni flat */
|
||||
.actions { display: none; gap: 8px; flex-wrap: wrap; margin-top: auto; }
|
||||
.btn-cmd { flex: 1; padding: 8px 10px; border: 1px solid var(--border-color); background: #010409 !important; border-radius: 3px; font-weight: 600; font-size: 0.75rem; cursor: pointer; color: var(--text-main) !important; font-family: 'JetBrains Mono', monospace; transition: border-color 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 5px; box-shadow: none !important; }
|
||||
.btn-cmd:hover { border-color: var(--text-main); }
|
||||
|
||||
/* Buttons */
|
||||
.actions { display: none; gap: 10px; flex-wrap: wrap; margin-top: auto; }
|
||||
.btn-cmd { flex: 1; padding: 10px 12px; border: none; border-radius: 12px; font-weight: 700; font-size: 0.8rem; cursor: pointer; color: white; transition: all 0.2s ease; box-shadow: 0 4px 6px rgba(0,0,0,0.1); display: flex; align-items: center; justify-content: center; gap: 5px; }
|
||||
.btn-cmd:hover { transform: translateY(-2px); filter: brightness(1.1); box-shadow: 0 6px 12px rgba(0,0,0,0.15); }
|
||||
.btn-cmd:active { transform: translateY(0); }
|
||||
/* Tabella LastHeard */
|
||||
.table-container { background: var(--card-bg); border-radius: 6px; border: 1px solid var(--border-color); overflow: hidden; max-height: 400px; overflow-y: auto; margin: 0 20px 40px 20px; box-shadow: none; }
|
||||
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.85rem; font-family: 'JetBrains Mono', monospace; }
|
||||
thead { background: var(--topbar-bg); position: sticky; top: 0; z-index: 1; border-bottom: 1px solid var(--border-color); }
|
||||
th { padding: 12px 15px; font-weight: 600; text-transform: uppercase; font-size: 0.75rem; color: var(--text-muted); }
|
||||
td { padding: 10px 15px; border-bottom: 1px solid var(--border-color); color: var(--text-main); }
|
||||
|
||||
.table-container { background: var(--card-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-radius: 20px; border: 1px solid var(--border-color); overflow: hidden; max-height: 400px; overflow-y: auto; box-shadow: var(--glass-shadow); margin: 0 20px 40px 20px; }
|
||||
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
|
||||
/* Animazioni Uniformate per i transiti */
|
||||
@keyframes pulse-border {
|
||||
0% { border-color: var(--border-color); }
|
||||
50% { border-color: var(--pulse-color, var(--primary)); }
|
||||
100% { border-color: var(--border-color); }
|
||||
}
|
||||
.tx-active-unified {
|
||||
animation: pulse-border 1.5s infinite !important;
|
||||
color: var(--text-main) !important;
|
||||
border-left: 4px solid var(--pulse-color, var(--primary)) !important;
|
||||
background: #010409 !important;
|
||||
}
|
||||
|
||||
/* Table Header Frosted Glass */
|
||||
thead { background: rgba(255, 255, 255, 0.9); position: sticky; top: 0; z-index: 1; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }
|
||||
body.dark-mode thead { background: rgba(15, 23, 42, 0.95); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); }
|
||||
th { padding: 15px; font-weight: 700; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.5px; color: var(--text-muted); }
|
||||
td { padding: 12px 15px; border-bottom: 1px solid var(--border-color); }
|
||||
/* La vecchia classe blink la teniamo solo per gli allarmi rossi dei demoni KO */
|
||||
@keyframes flat-blink { 0% { border-color: var(--border-color); } 50% { border-color: var(--danger); } 100% { border-color: var(--border-color); } }
|
||||
.blink { animation: flat-blink 1.5s infinite; color: var(--danger) !important; }
|
||||
|
||||
@keyframes pulse-glow { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } }
|
||||
.blink { animation: pulse-glow 1.5s infinite; color: var(--danger) !important; font-weight: 800 !important; background: rgba(239, 68, 68, 0.1) !important; border-left-color: var(--danger) !important; }
|
||||
@keyframes tx-glow { 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5); } 70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); } 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } }
|
||||
.tx-active { animation: tx-glow 1.5s infinite; font-weight: 800 !important; color: var(--text-main) !important; background: rgba(59, 130, 246, 0.25) !important; border-left-color: var(--primary) !important; }
|
||||
/* Finestre Modali (Popup) */
|
||||
.modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(1, 4, 9, 0.85); z-index:1000; align-items:center; justify-content:center; }
|
||||
.modal-content { background:var(--card-bg); border:1px solid var(--border-color); padding:25px; border-radius:6px; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 30px rgba(0,0,0,0.8); }
|
||||
|
||||
/* Modals (Dark Glass) */
|
||||
.modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); backdrop-filter:blur(5px); -webkit-backdrop-filter:blur(5px); z-index:1000; align-items:center; justify-content:center; }
|
||||
.modal-content { background:var(--card-bg); backdrop-filter:blur(20px); border:1px solid var(--border-color); padding:30px; border-radius:24px; box-shadow:0 25px 50px -12px rgba(0,0,0,0.5); max-height: 90vh; overflow-y: auto; }
|
||||
/* Elementi Input e Form */
|
||||
.auth-btn { background: #010409; color: var(--text-main); border: 1px solid var(--border-color); padding: 6px 12px; border-radius: 4px; font-weight: 600; cursor: pointer; font-size: 0.8rem; font-family: 'JetBrains Mono', monospace; transition: border-color 0.2s; }
|
||||
.auth-btn:hover { border-color: var(--text-main); }
|
||||
input, select { background: #010409; border: 1px solid var(--border-color); color: var(--text-main); padding: 8px 12px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; outline: none; }
|
||||
input:focus { border-color: var(--primary); }
|
||||
input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
option { background: var(--bg-main); color: var(--text-main); }
|
||||
|
||||
/* Auth Buttons Style */
|
||||
.auth-btn { background: var(--text-main); color: var(--bg-gradient); border: none; padding: 8px 16px; border-radius: 20px; font-weight: 700; cursor: pointer; font-size: 0.85rem; transition: 0.2s; }
|
||||
.auth-btn:hover { opacity: 0.8; }
|
||||
input, select { background: rgba(128,128,128,0.1); border: 1px solid var(--border-color); color: var(--text-main); padding: 10px 15px; border-radius: 12px; font-family: inherit; outline: none; transition: 0.2s; }
|
||||
input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); }
|
||||
input:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
/* Dropdown Fix in Dark Mode */
|
||||
option { background: #ffffff; color: #1e293b; }
|
||||
/* --- OTTIMIZZAZIONE MOBILE (PWA / SMARTPHONE) --- */
|
||||
/* Ottimizzazione Mobile */
|
||||
@media (max-width: 768px) {
|
||||
#top-bar-container { top: 10px; padding: 0 10px; }
|
||||
#top-bar {
|
||||
flex-direction: column;
|
||||
border-radius: 24px;
|
||||
padding: 15px;
|
||||
gap: 12px;
|
||||
}
|
||||
.title-brand { font-size: 1.15rem; text-align: center; width: 100%; }
|
||||
|
||||
/* Contenitore dei tasti lingua/tema */
|
||||
#top-bar > div {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
/* Contenitore Bottoni Login / Admin */
|
||||
#auth-container {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 12px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
/* Username centrato */
|
||||
#auth-container > span {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Bottoni ingranditi perfetti per il touch */
|
||||
.auth-btn {
|
||||
flex: 1 1 auto;
|
||||
min-width: 100px;
|
||||
padding: 12px !important;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.theme-switch {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
#top-bar { flex-direction: column; padding: 15px; gap: 12px; border-radius: 8px; }
|
||||
.title-brand { font-size: 1.1rem; text-align: center; width: 100%; }
|
||||
#top-bar > div { width: 100%; flex-wrap: wrap; justify-content: center; gap: 8px !important; }
|
||||
#auth-container { width: 100%; justify-content: center; flex-wrap: wrap; padding-top: 12px; border-top: 1px solid var(--border-color); }
|
||||
#auth-container > span { width: 100%; text-align: center; margin-bottom: 8px; font-family: 'JetBrains Mono', monospace; }
|
||||
.auth-btn, .theme-switch { flex: 1 1 auto; text-align: center; }
|
||||
}
|
||||
body.dark-mode option { background: #0f172a; color: #f8fafc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -162,7 +126,6 @@
|
||||
<div class="title-brand">FLEET CONTROL CONSOLE</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<button class="theme-switch" id="lang-btn" onclick="toggleLang()" data-i18n-title="ttLang">🇮🇹 ITA</button>
|
||||
<button class="theme-switch" id="theme-btn" onclick="toggleTheme()" data-i18n-title="ttTheme">🌙 DARK</button>
|
||||
<button class="theme-switch" id="push-btn" onclick="subscribeToPush()">🔔 PUSH</button>
|
||||
<div id="auth-container" style="display:flex; align-items:center; gap:8px;"></div>
|
||||
</div>
|
||||
@@ -460,12 +423,6 @@
|
||||
let currentResetHatId = null;
|
||||
let confirmActionCallback = null;
|
||||
|
||||
function toggleTheme() {
|
||||
const isDark = document.body.classList.toggle('dark-mode');
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
document.getElementById('theme-btn').innerText = isDark ? t('themeLight') : t('themeDark');
|
||||
}
|
||||
|
||||
// --- NEW GLOBAL GLASSMORPHISM POPUPS ---
|
||||
function customAlert(title, desc, isError = false) {
|
||||
const color = isError ? 'var(--danger)' : 'var(--success)';
|
||||
@@ -585,11 +542,6 @@
|
||||
const grid = document.getElementById('client-grid');
|
||||
const authContainer = document.getElementById('auth-container');
|
||||
|
||||
if (localStorage.getItem('theme') === 'dark') {
|
||||
document.body.classList.add('dark-mode');
|
||||
document.getElementById('theme-btn').innerText = t('themeLight');
|
||||
} else { document.getElementById('theme-btn').innerText = t('themeDark'); }
|
||||
|
||||
const role = sessionStorage.getItem('user_role');
|
||||
const allowed = sessionStorage.getItem('allowed_nodes') || "";
|
||||
|
||||
@@ -716,18 +668,31 @@
|
||||
if (altDiv) {
|
||||
altDiv.style.display = "block"; altDiv.innerText = telemetryObj.alt;
|
||||
let altText = telemetryObj.alt.toUpperCase();
|
||||
if (altText.includes("NXDN")) activeModeColor = "#10b981"; else if (altText.includes("YSF")) activeModeColor = "#8b5cf6"; else if (altText.includes("D-STAR")) activeModeColor = "#06b6d4"; else if (altText.includes("P25")) activeModeColor = "#f59e0b";
|
||||
|
||||
// Assegna il colore in base al modo
|
||||
if (altText.includes("NXDN")) activeModeColor = "#10b981";
|
||||
else if (altText.includes("YSF")) activeModeColor = "#8b5cf6";
|
||||
else if (altText.includes("D-STAR")) activeModeColor = "#06b6d4";
|
||||
else if (altText.includes("P25")) activeModeColor = "#f59e0b";
|
||||
|
||||
isTx = altText.includes("🟢") || altText.includes("🟣") || altText.includes("🔵") || altText.includes("🟠");
|
||||
altDiv.style.setProperty('color', activeModeColor, 'important');
|
||||
altDiv.style.setProperty('border-left', `4px solid ${activeModeColor}`, 'important');
|
||||
if (isTx) { altDiv.classList.add('blink'); } else { altDiv.classList.remove('blink'); }
|
||||
|
||||
// Passiamo il colore al CSS per l'animazione
|
||||
altDiv.style.setProperty('--pulse-color', activeModeColor);
|
||||
if (isTx) {
|
||||
altDiv.classList.add('tx-active-unified');
|
||||
} else {
|
||||
altDiv.classList.remove('tx-active-unified');
|
||||
altDiv.style.setProperty('color', 'var(--text-muted)', 'important');
|
||||
altDiv.style.setProperty('border-left', `4px solid var(--border-color)`, 'important');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (altDiv) altDiv.style.display = "none";
|
||||
if (tsContainer) tsContainer.style.display = "flex";
|
||||
|
||||
let netObj = data.networks && data.networks[c.id.toLowerCase()] ? data.networks[c.id.toLowerCase()] : {ts1: "", ts2: ""};
|
||||
activeModeColor = "var(--primary)";
|
||||
activeModeColor = "var(--primary)"; // Default Blu per il DMR
|
||||
|
||||
if (ts1Div && ts2Div) {
|
||||
[ts1Div, ts2Div].forEach((div, idx) => {
|
||||
@@ -737,15 +702,15 @@
|
||||
const fullLabel = netName ? `${baseLabel} [${netName}]` : baseLabel;
|
||||
|
||||
div.innerText = `${fullLabel}: ${val}`;
|
||||
div.style.setProperty('--pulse-color', activeModeColor);
|
||||
|
||||
if (val.includes("🎙️")) {
|
||||
isTx = true;
|
||||
div.classList.add('tx-active');
|
||||
div.classList.add('tx-active-unified');
|
||||
} else {
|
||||
div.classList.remove('tx-active');
|
||||
div.style.setProperty('color', 'var(--text-main)', 'important');
|
||||
div.style.setProperty('border-left-color', 'var(--primary)', 'important');
|
||||
div.style.setProperty('background', 'rgba(59, 130, 246, 0.1)', 'important');
|
||||
div.classList.remove('tx-active-unified');
|
||||
div.style.setProperty('color', 'var(--text-muted)', 'important');
|
||||
div.style.setProperty('border-left', '4px solid var(--border-color)', 'important');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user