Loading...
-
- 📡 Lastheard
-
-
-
-
-
- | Time |
- Repeater |
- Mode |
- Slot |
- Callsign |
- Target / TG |
- Duration |
- BER |
- RSSI |
-
-
-
-
+
+
+
🎯 TOP TALKGROUPS
+
Loading...
+
+
+
+
🗣️ TOP CALLSIGNS
+
Loading...
+
+
+
+
+
--s
+
⏱️ AVERAGE DURATION
+
+
+
+
--
+
📡 TRANSITS TODAY
+
+
+
+
+
+
+ 📡 Lastheard
+
+
+
+
+
+ | Time |
+ Repeater |
+ Mode |
+ Slot |
+ Callsign |
+ Target / TG |
+ Duration |
+ BER |
+ RSSI |
+
+
+
+
+
@@ -475,11 +504,27 @@
}
// --- HELPER DI SICUREZZA ---
- // esc(): escape HTML per tutto ciò che arriva da MQTT / database / utenti prima di finire in innerHTML
function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
- // q(): letterale JS sicuro da usare dentro onclick="...": onclick="fn(${q(valore)})"
function q(s) { return esc(JSON.stringify(String(s ?? ''))); }
- const HANG_TIME_MS = 180000; // tempo massimo in cui la card resta "accesa" dopo l'ultimo TX
+ const HANG_TIME_MS = 180000;
+
+ // --- GESTIONE SCHEDE (TABS) ---
+ function switchView(viewName) {
+ document.getElementById('view-home').style.display = 'none';
+ document.getElementById('view-nodes').style.display = 'none';
+ document.getElementById('tab-home').classList.remove('active');
+ document.getElementById('tab-nodes').classList.remove('active');
+
+ document.getElementById(`view-${viewName}`).style.display = 'block';
+ document.getElementById(`tab-${viewName}`).classList.add('active');
+
+ localStorage.setItem('fleetDefaultView', viewName);
+ }
+
+ window.addEventListener('DOMContentLoaded', () => {
+ const savedView = localStorage.getItem('fleetDefaultView') || 'home';
+ switchView(savedView);
+ });
// --- DIAGNOSTICA: "ultimo messaggio ricevuto" per nodo (MMDVM / gateway / agent) ---
function fmtAge(sec) {
@@ -502,8 +547,6 @@
const upSec = data.uptime || 0;
let html = '';
- // MMDVM: pubblica eventi (TX, cambio modo), quindi su un nodo tranquillo l'età può crescere senza problemi.
- // Si segnala solo il caso "mai ricevuto nulla" con nodo online e server attivo da un po'.
const warnM = ages.mmdvm === undefined && isOnline && upSec > 600;
const tipM = it ? `Ultimo evento dal topic mmdvm/${c.id}/json (TX, cambio modo...). Su un nodo tranquillo può passare molto tempo.`
: `Last event from topic mmdvm/${c.id}/json (TX, mode change...). It can be a long time on a quiet node.`;
@@ -511,13 +554,10 @@
: `No MMDVM event received since the server started (${fmtAge(upSec)}): check that MMDVMHost is connected to the broker.`;
html += seenItem('MMDVM', ages.mmdvm, warnM ? tipMW : tipM, warnM);
- // Gateway: solo se il nodo ne ha mai pubblicati
if (ages.gateway !== undefined) {
html += seenItem('GW', ages.gateway, it ? 'Ultimo evento dai gateway (DMR/NXDN/YSF/P25/D-Star)' : 'Last event from the gateways (DMR/NXDN/YSF/P25/D-Star)', false);
}
- // Agent: heartbeat periodico, se tace a lungo con nodo "online" qualcosa non va
- // Soglia in base all'intervallo di telemetria dichiarato dall'agente (default 60 s): 3 intervalli, minimo 180 s
const hObj = (data.health && data.health[c.id.toLowerCase()]) || {};
const iv = Number(hObj.interval) > 0 ? Number(hObj.interval) : 60;
const warnA = isOnline && (ages.health === undefined ? upSec > Math.max(300, iv * 5) : ages.health > Math.max(180, iv * 3));
@@ -532,7 +572,7 @@
// --- GLOBAL VARIABLES ---
let clients = [];
- let me = { logged_in: false, user: '', role: '', allowed_nodes: '' }; // stato reale della sessione (da /api/me)
+ let me = { logged_in: false, user: '', role: '', allowed_nodes: '' };
let isAuthenticated = false;
let usersCache = {};
let globalHealthData = {};
@@ -596,8 +636,7 @@
}
function confirmReboot(id) { sendCommand(id, 'REBOOT', `🔄 ${t('titleBoot')}`, `${t('confBoot')}
${id.toUpperCase()}?`, "var(--danger)"); }
- function confirmUpdateAgent(id) { sendCommand(id, 'UPDATE_AGENT', `⬇️ UPDATE OTA`, `Confermi l'aggiornamento dello script tramite git pull sul nodo
${id.toUpperCase()}?`, "#2563eb");
- }
+ function confirmUpdateAgent(id) { sendCommand(id, 'UPDATE_AGENT', `⬇️ UPDATE OTA`, `Confermi l'aggiornamento dello script tramite git pull sul nodo
${id.toUpperCase()}?`, "#2563eb"); }
function confirmHatReset(id) {
currentResetHatId = id;
@@ -648,7 +687,6 @@
const response = await fetch('/api/clients');
clients = await response.json();
- // Gli id nodo finiscono in id HTML e onclick: accettiamo solo caratteri sicuri
clients = clients.filter(c => /^[A-Za-z0-9_\-]+$/.test(String(c.id)));
const statSelect = document.getElementById('stat-node-filter');
@@ -769,17 +807,15 @@
const statusDiv = document.getElementById(`status-${c.id}`); const cardDiv = document.getElementById(`card-${c.id}`);
if (!statusDiv || !cardDiv) return;
- // 1. PRIMA CALCOLIAMO SE È ONLINE
let stateValue = data.states[c.id.toLowerCase()] || data.states[c.id] || "OFFLINE";
stateValue = String(stateValue).trim().toUpperCase(); statusDiv.innerText = stateValue;
const isOnline = !stateValue.includes("OFF") && stateValue !== "";
renderSeen(c, data, isOnline);
- // 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
+ const typeBadge = document.getElementById(`type-badge-${c.id}`);
if (genObj && isOnline) {
nameSpan.innerText = genObj.callsign;
@@ -788,7 +824,6 @@
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") {
@@ -806,10 +841,9 @@
idBadge.style.color = "var(--text-muted)";
idBadge.style.borderColor = "var(--border-color)";
- if (typeBadge) typeBadge.style.display = 'none'; // Nascondilo se il nodo va offline
+ if (typeBadge) typeBadge.style.display = 'none';
}
- // 3. TELEMETRIA E STATO (PULITO DAI DUPLICATI)
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 };
@@ -840,14 +874,10 @@
activeModeColor = "var(--primary)";
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) => {
- // Se è simplex, saltiamo del tutto l'aggiornamento grafico del TS1
if (isSimplex && idx === 0) return;
const val = String((idx === 0 ? telemetryObj.ts1 : telemetryObj.ts2) ?? ''); const netName = idx === 0 ? netObj.ts1 : netObj.ts2;
@@ -937,15 +967,12 @@
if (isOnline) {
cardDiv.classList.add('online'); statusDiv.classList.remove('status-offline'); cardDiv.style.opacity = "1"; cardDiv.style.filter = "none";
- // --- INIZIO LOGICA SMART HANG-TIME ---
- if (isTx) cardDiv.dataset.lastTx = Date.now(); // Memorizza quando è avvenuto l'ultimo TX
+ if (isTx) cardDiv.dataset.lastTx = Date.now();
let timeSinceTx = Date.now() - (parseInt(cardDiv.dataset.lastTx) || 0);
- // Accendi se in TX, OPPURE se in Hang Time (ma spegni a forza dopo HANG_TIME_MS di inattività)
let showActiveColor = isTx || (!isIdle && timeSinceTx < HANG_TIME_MS);
cardDiv.style.setProperty('border-top-color', showActiveColor ? activeModeColor : "var(--border-color)", 'important');
- // --- FINE LOGICA ---
if(isTx) {
let shadowRGB = '59, 130, 246';
if (activeModeColor === '#10b981') shadowRGB = '16, 185, 129';
@@ -971,7 +998,7 @@
const time = row[0] ? row[0].split(' ')[1] : "--:--";
const clientId = row[1]; const protocol = row[2] || "DMR"; let source = row[3] || "---"; const target = row[4] || "---";
const rawSlot = row[5]; const source_ext = row[8];
- const rawRssi = row[9]; // Estrai il nuovo valore
+ const rawRssi = row[9];
let sourceHtml = esc(source);
if (source_ext && source_ext.trim() !== "") { sourceHtml += `
/${esc(source_ext)}`; }
@@ -1030,7 +1057,6 @@
if (!networkLogs[clientId]) networkLogs[clientId] = "";
networkLogs[clientId] += `
[${esc(time)}] [${esc(protocol)}] ${esc(source.replace("🌐", "").trim())}
`;
} else {
- // Aggiunto il TD per l'RSSI alla fine
tableHTML += `
| ${esc(time)} | ${esc(String(clientId).toUpperCase())} | ${esc(protocol)} | ${esc(slotDisplay)} | ${sourceHtml} | ${esc(target.trim())} | ${esc(row[6])}s | ${esc(row[7])}% | ${rssiDisplay} |
`;
}
});
@@ -1097,7 +1123,6 @@
// --- SETTINGS ---
function triggerGlobalEmergency() {
let uniqueProfiles = {};
- // Raccoglie tutti i profili unici trovati nei nodi online
for (const id in globalHealthData) {
if (globalHealthData[id] && globalHealthData[id].profiles) {
for (const [k, v] of Object.entries(globalHealthData[id].profiles)) {
@@ -1215,7 +1240,7 @@
// --- PUSH NOTIFICATIONS ---
async function subscribeToPush() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) { customAlert("Error", "Push notifications are not supported by this browser.", true); return; }
- if (!me.logged_in) { openLoginModal(); return; } // le notifiche si attivano solo da loggati
+ if (!me.logged_in) { openLoginModal(); return; }
const permission = await Notification.requestPermission();
if (permission !== 'granted') { customAlert("Error", "Push notifications permission denied.", true); return; }
try {
@@ -1236,7 +1261,6 @@
checkPushStatus();
// --- AGGIORNAMENTI PERIODICI ---
- // Fallback se il WebSocket non è disponibile + spegne l'hang time scaduto anche senza nuovi eventi
setInterval(refreshStates, 15000);
setInterval(() => { refreshLogs(); refreshStats(); }, 60000);
@@ -1252,8 +1276,8 @@
if (badge) { if (data.connected) { badge.innerText = "MQTT: ONLINE"; badge.className = "mqtt-badge mqtt-online"; } else { badge.innerText = "MQTT: OFFLINE"; badge.className = "mqtt-badge mqtt-offline"; } }
});
socket.on('connect', () => { refreshStates(); refreshLogs(); refreshStats(); });
- socket.on('dati_aggiornati', refreshStatesSoon); // stato nodi / telemetria
- socket.on('log_aggiornato', refreshLogsSoon); // nuovi ascolti -> lastheard + statistiche
+ socket.on('dati_aggiornati', refreshStatesSoon);
+ socket.on('log_aggiornato', refreshLogsSoon);
}