+
🔄 GLOBAL UPDATE 🔄
Do you want to request updated data from all nodes in the fleet?
@@ -447,7 +362,6 @@
// --- 1. TRANSLATION SYSTEM (i18n) ---
const i18n = {
en: {
- themeLight: "☀️ LIGHT", themeDark: "🌙 DARK",
lastTransits: "Lastheard", loginTitle: "🔒 System Login",
thTime: "Time", thRep: "Repeater", thMode: "Mode", thCall: "Callsign", thDur: "Duration",
thUser: "User", thRole: "Role", thNodes: "Nodes", thActs: "Actions",
@@ -472,12 +386,11 @@
titleSwitch: "SWITCH PROFILE", confSwitch: "Are you sure you want to switch to", titleBoot: "SYSTEM REBOOT", confBoot: "Are you sure you want to REBOOT node ",
ttReqCfg: "Request immediate data update", ttGlobal: "Force profile switch on network", ttAdmin: "User and system management", ttPass: "Change your password", ttLogout: "Log out",
ttProfA: "Switch to Profile A", ttProfB: "Switch to Profile B", ttTgOn: "Enable Telegram notifications", ttTgOff: "Disable Telegram notifications", ttSvc: "Manage system daemons", ttFile: "Edit .ini configuration files", ttHat: "Send physical reset to modem", ttBoot: "Reboot node operating system",
- ttSwitchTo: "Switch to profile: ", ttSvcStart: "Start service", ttSvcRestart: "Restart service", ttSvcEdit: "Edit configuration", ttSvcStop: "Stop service", ttLang: "Change language", ttTheme: "Change visual theme",
+ ttSwitchTo: "Switch to profile: ", ttSvcStart: "Start service", ttSvcRestart: "Restart service", ttSvcEdit: "Edit configuration", ttSvcStop: "Stop service", ttLang: "Change language",
statTopTG: "🎯 TOP TALKGROUPS", statTopCall: "🗣️ TOP CALLSIGNS", statLoading: "Loading...", statAvgDur: "⏱️ AVERAGE DURATION", statTodayTx: "📡 TRANSITS TODAY", statNoData: "No data",
statTitle: "DAILY STATISTICS", statAllNodes: "ALL NODES"
},
it: {
- themeLight: "☀️ CHIARO", themeDark: "🌙 SCURO",
lastTransits: "Ultimi ascolti", loginTitle: "🔒 Login di Sistema",
thTime: "Ora", thRep: "Ripetitore", thMode: "Modo", thCall: "Nominativo", thDur: "Durata",
thUser: "Utente", thRole: "Ruolo", thNodes: "Nodi", thActs: "Azioni",
@@ -502,7 +415,7 @@
titleSwitch: "CAMBIO PROFILO", confSwitch: "Sei sicuro di voler passare al", titleBoot: "RIAVVIO SISTEMA", confBoot: "Sei sicuro di voler RIAVVIARE il nodo ",
ttReqCfg: "Richiedi aggiornamento dati", ttGlobal: "Forza cambio profilo globale", ttAdmin: "Gestione utenti e sistema", ttPass: "Cambia la tua password", ttLogout: "Disconnetti",
ttProfA: "Passa al Profilo A", ttProfB: "Passa al Profilo B", ttTgOn: "Abilita notifiche Telegram", ttTgOff: "Disabilita notifiche Telegram", ttSvc: "Gestisci demoni di sistema", ttFile: "Modifica file .ini da remoto", ttHat: "Reset fisico scheda modem", ttBoot: "Riavvia sistema operativo",
- ttSwitchTo: "Passa al profilo: ", ttSvcStart: "Avvia servizio", ttSvcRestart: "Riavvia servizio", ttSvcEdit: "Modifica configurazione", ttSvcStop: "Ferma servizio", ttLang: "Cambia lingua", ttTheme: "Cambia tema visivo",
+ ttSwitchTo: "Passa al profilo: ", ttSvcStart: "Avvia servizio", ttSvcRestart: "Riavvia servizio", ttSvcEdit: "Modifica configurazione", ttSvcStop: "Ferma servizio", ttLang: "Cambia lingua",
statTopTG: "🎯 TOP TALKGROUPS", statTopCall: "🗣️ TOP CALLSIGNS", statLoading: "Caricamento...", statAvgDur: "⏱️ DURATA MEDIA", statTodayTx: "📡 TRANSITI OGGI", statNoData: "Nessun dato",
statTitle: "STATISTICHE GIORNALIERE", statAllNodes: "TUTTA LA RETE"
}
@@ -513,26 +426,20 @@
function applyTranslations() {
document.getElementById('lang-btn').innerText = currentLang === 'it' ? "🇬🇧 ENG" : "🇮🇹 ITA";
-
- // Translate innerHTML
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (i18n[currentLang][key]) el.innerHTML = i18n[currentLang][key];
});
-
- // Translate tooltips (titles)
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
if (i18n[currentLang][key]) el.title = i18n[currentLang][key];
});
-
- // Translate placeholders
const newPassInput = document.getElementById('new-pass');
if (newPassInput && editingUserId) newPassInput.placeholder = t('phNewPass');
else if (newPassInput) newPassInput.placeholder = t('phPass');
}
- // --- GLOBAL VARIABLES & THEMES ---
+ // --- GLOBAL VARIABLES ---
let clients = [];
let isAuthenticated = sessionStorage.getItem('is_admin') === 'true';
let globalHealthData = {};
@@ -540,7 +447,7 @@
let currentResetHatId = null;
let confirmActionCallback = null;
- // --- NEW GLOBAL GLASSMORPHISM POPUPS ---
+ // --- POPUPS & ALERTS ---
function customAlert(title, desc, isError = false) {
const color = isError ? 'var(--danger)' : 'var(--success)';
document.getElementById('alert-title').innerHTML = title;
@@ -575,7 +482,6 @@
const title = customTitle || t('titleAction');
const msg = customMsg || `${t('confOp')}
${clientId.toUpperCase()}?`;
const color = customColor || "var(--primary)";
-
customConfirm(title, msg, color, async () => {
try {
const res = await fetch('/api/command', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId, type }) });
@@ -589,38 +495,28 @@
function confirmSwitch(id, mode) {
const data = globalHealthData[id.toLowerCase()];
let profileName = mode === 'A' ? "PROFILE A" : "PROFILE B";
- if (data && data.profiles && data.profiles[mode]) {
- profileName = data.profiles[mode];
- }
+ if (data && data.profiles && data.profiles[mode]) profileName = data.profiles[mode];
const title = `🔄 ${t('titleSwitch')}`;
const msg = `${t('confSwitch')}
${profileName} ->
${id.toUpperCase()}?`;
const color = mode === 'A' ? "var(--accent)" : "#eab308";
sendCommand(id, mode, title, msg, color);
}
- function confirmReboot(id) {
- const title = `🔄 ${t('titleBoot')}`;
- const msg = `${t('confBoot')}
${id.toUpperCase()}?`;
- sendCommand(id, 'REBOOT', title, msg, "var(--danger)");
- }
+ function confirmReboot(id) { sendCommand(id, 'REBOOT', `🔄 ${t('titleBoot')}`, `${t('confBoot')}
${id.toUpperCase()}?`, "var(--danger)"); }
function confirmHatReset(id) {
currentResetHatId = id;
document.getElementById('reset-hat-desc').innerText = `${t('confHat')}${id.toUpperCase()}?`;
document.getElementById('reset-hat-modal').style.display = 'flex';
}
- function closeHatResetModal() {
- document.getElementById('reset-hat-modal').style.display = 'none';
- currentResetHatId = null;
- }
+ function closeHatResetModal() { document.getElementById('reset-hat-modal').style.display = 'none'; currentResetHatId = null; }
+
async function executeHatReset() {
if (!currentResetHatId) return;
const clientId = currentResetHatId;
closeHatResetModal();
try {
- const res = await fetch('/api/command', {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: clientId, type: 'RESET_HAT' })
- });
+ const res = await fetch('/api/command', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: clientId, type: 'RESET_HAT' }) });
const data = await res.json();
if(!data.success) { customAlert(t('titleError'), data.error, true); }
refreshStates();
@@ -632,10 +528,8 @@
async function executeUpdateRequest() {
closeUpdateModal();
- try {
- await fetch('/api/update_nodes', { method: 'POST' });
- customAlert(t('titleSuccess'), t('alertUpdateOk'));
- } catch (e) { console.error(e); }
+ try { await fetch('/api/update_nodes', { method: 'POST' }); customAlert(t('titleSuccess'), t('alertUpdateOk')); }
+ catch (e) { console.error(e); }
}
function sendTgCommand(clientId, comando) {
@@ -655,21 +549,17 @@
applyTranslations();
try {
const response = await fetch('/api/clients');
- clients = await response.json(); // <-- CORREZIONE 2: Ora carichiamo i dati prima di usarli!
+ clients = await response.json();
- // Popola il menu a tendina delle statistiche
const statSelect = document.getElementById('stat-node-filter');
if (statSelect) {
let opts = `
`;
- clients.forEach(c => {
- opts += `
`;
- });
+ clients.forEach(c => { opts += `
`; });
statSelect.innerHTML = opts;
}
const grid = document.getElementById('client-grid');
const authContainer = document.getElementById('auth-container');
-
const role = sessionStorage.getItem('user_role');
const allowed = sessionStorage.getItem('allowed_nodes') || "";
@@ -731,7 +621,7 @@
-
+
` : ''}
`;
@@ -741,16 +631,11 @@
} catch (e) { console.error(e); }
}
- // --- LOGIN MODAL LOGIC ---
- function openLoginModal() {
- document.getElementById('login-modal').style.display = 'flex';
- setTimeout(() => document.getElementById('modal-username').focus(), 100);
- }
- function closeLoginModal() {
- document.getElementById('login-modal').style.display = 'none';
- document.getElementById('modal-username').value = ''; document.getElementById('modal-password').value = '';
- }
+ // --- LOGIN ---
+ function openLoginModal() { document.getElementById('login-modal').style.display = 'flex'; setTimeout(() => document.getElementById('modal-username').focus(), 100); }
+ function closeLoginModal() { document.getElementById('login-modal').style.display = 'none'; document.getElementById('modal-username').value = ''; document.getElementById('modal-password').value = ''; }
function handleLoginEnter(e) { if (e.key === 'Enter') performLogin(); }
+
async function performLogin() {
const user = document.getElementById('modal-username').value; const pass = document.getElementById('modal-password').value;
if (!user || !pass) return;
@@ -758,45 +643,33 @@
const data = await res.json();
if (res.ok) {
sessionStorage.setItem('is_admin', 'true'); sessionStorage.setItem('user_name', user); sessionStorage.setItem('user_role', data.role); sessionStorage.setItem('allowed_nodes', data.allowed_nodes); location.reload();
- } else {
- customAlert(t('titleError'), t('msgLoginFail'), true);
- }
+ } else { customAlert(t('titleError'), t('msgLoginFail'), true); }
}
function logout() { sessionStorage.clear(); location.reload(); }
async function refreshStates() {
try {
- const res = await fetch('/api/states');
- const data = await res.json();
+ const res = await fetch('/api/states'); const data = await res.json();
clients.forEach(c => {
- 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;
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 !== "";
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 };
- const tsContainer = document.getElementById(`ts-container-${c.id}`);
- const ts1Div = document.getElementById(`dmr-ts1-${c.id}`);
- const ts2Div = document.getElementById(`dmr-ts2-${c.id}`);
- const altDiv = document.getElementById(`dmr-alt-${c.id}`);
+ const tsContainer = document.getElementById(`ts-container-${c.id}`); const ts1Div = document.getElementById(`dmr-ts1-${c.id}`); const ts2Div = document.getElementById(`dmr-ts2-${c.id}`); const altDiv = document.getElementById(`dmr-alt-${c.id}`);
- let isTx = false;
- let activeModeColor = "var(--success)";
- let isIdle = telemetryObj.idle === true;
+ let isTx = false; let activeModeColor = "var(--success)"; let isIdle = telemetryObj.idle === true;
if (telemetryObj.alt && telemetryObj.alt !== "") {
if (tsContainer) tsContainer.style.display = "none";
if (altDiv) {
- altDiv.style.display = "block"; altDiv.innerText = telemetryObj.alt;
- let altText = telemetryObj.alt.toUpperCase();
+ altDiv.style.display = "block"; altDiv.innerText = telemetryObj.alt; let altText = telemetryObj.alt.toUpperCase();
- // 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";
@@ -804,72 +677,42 @@
isTx = altText.includes("🟢") || altText.includes("🟣") || altText.includes("🔵") || altText.includes("🟠");
- // 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');
- }
+ 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)"; // Default Blu per il DMR
+ activeModeColor = "var(--primary)";
if (ts1Div && ts2Div) {
[ts1Div, ts2Div].forEach((div, idx) => {
- const val = idx === 0 ? telemetryObj.ts1 : telemetryObj.ts2;
- const netName = idx === 0 ? netObj.ts1 : netObj.ts2;
- const baseLabel = `TS${idx + 1}`;
- const fullLabel = netName ? `${baseLabel} [${netName}]` : baseLabel;
-
+ 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}`;
div.innerText = `${fullLabel}: ${val}`;
div.style.setProperty('--pulse-color', activeModeColor);
- if (val.includes("🎙️")) {
- isTx = true;
- div.classList.add('tx-active-unified');
- } else {
- 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');
- }
+ if (val.includes("🎙️")) { isTx = true; div.classList.add('tx-active-unified'); }
+ else { 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'); }
});
}
}
let healthObj = data.health && data.health[c.id.toLowerCase()];
- 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) {
healthContainer.style.display = 'flex';
let cpu = healthObj.cpu; cpuSpan.innerText = cpu; cpuSpan.style.color = cpu < 50 ? 'var(--success)' : (cpu < 80 ? '#f59e0b' : 'var(--danger)');
-
- // FIXED TEMPERATURE VARIABLE NAME HERE
- 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)');
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 btnA = document.getElementById(`btn-profA-${c.id}`); const btnB = document.getElementById(`btn-profB-${c.id}`);
-
- if (btnA) {
- if (btnA.innerText !== profA) btnA.innerText = profA;
- btnA.title = `${t('ttSwitchTo')}${profA}`;
- }
- if (btnB) {
- if (btnB.innerText !== profB) btnB.innerText = profB;
- btnB.title = `${t('ttSwitchTo')}${profB}`;
- }
-
+ if (btnA) { btnA.innerText = profA; btnA.title = `${t('ttSwitchTo')}${profA}`; }
+ if (btnB) { btnB.innerText = profB; btnB.title = `${t('ttSwitchTo')}${profB}`; }
} else { if (healthContainer) healthContainer.style.display = 'none'; }
globalHealthData[c.id.toLowerCase()] = healthObj;
@@ -887,30 +730,19 @@
}
if (isOnline) {
- cardDiv.classList.add('online'); statusDiv.classList.remove('status-offline');
- cardDiv.style.opacity = "1"; cardDiv.style.filter = "none";
-
- let targetBorderColor = activeModeColor;
- if (!isTx && isIdle) { targetBorderColor = "var(--border-color)"; }
- cardDiv.style.setProperty('border-top-color', targetBorderColor, 'important');
-
+ 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');
if(isTx) {
- // Creiamo un alone luminoso che sia SEMPRE dello stesso colore del modo attivo
- let shadowRGB = '59, 130, 246'; // Blu di default (per DMR e var(--primary))
- if (activeModeColor === '#10b981') shadowRGB = '16, 185, 129'; // Verde (NXDN)
- else if (activeModeColor === '#8b5cf6') shadowRGB = '139, 92, 246'; // Viola (YSF)
- else if (activeModeColor === '#06b6d4') shadowRGB = '6, 182, 212'; // Ciano (D-STAR)
- else if (activeModeColor === '#f59e0b') shadowRGB = '245, 158, 11'; // Arancione (P25)
-
+ let shadowRGB = '59, 130, 246';
+ if (activeModeColor === '#10b981') shadowRGB = '16, 185, 129';
+ else if (activeModeColor === '#8b5cf6') shadowRGB = '139, 92, 246';
+ else if (activeModeColor === '#06b6d4') shadowRGB = '6, 182, 212';
+ else if (activeModeColor === '#f59e0b') shadowRGB = '245, 158, 11';
cardDiv.style.boxShadow = `0 0 20px rgba(${shadowRGB}, 0.5)`;
- } else {
- cardDiv.style.boxShadow = 'var(--glass-shadow)';
- }
+ } else { cardDiv.style.boxShadow = 'none'; }
} else {
- cardDiv.classList.remove('online'); statusDiv.classList.add('status-offline');
- cardDiv.style.opacity = "0.7"; cardDiv.style.filter = "grayscale(80%)";
- cardDiv.style.setProperty('border-top-color', '#64748b', 'important');
- cardDiv.style.boxShadow = 'var(--glass-shadow)';
+ cardDiv.classList.remove('online'); statusDiv.classList.add('status-offline'); cardDiv.style.opacity = "0.7"; cardDiv.style.filter = "grayscale(80%)";
+ cardDiv.style.setProperty('border-top-color', '#64748b', 'important'); cardDiv.style.boxShadow = 'none';
}
});
document.getElementById('last-update').innerText = "Sync: " + new Date().toLocaleTimeString();
@@ -923,17 +755,9 @@
const tbody = document.getElementById('log-body'); let tableHTML = ""; let networkLogs = {};
logs.forEach(row => {
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]; // <--- NUOVO CAMPO DAL BACKEND
-
- // Formattiamo il source_ext se presente e non vuoto
- if (source_ext && source_ext.trim() !== "") {
- source = `${source}
/${source_ext}`;
- }
+ 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];
+ if (source_ext && source_ext.trim() !== "") { source = `${source}
/${source_ext}`; }
const slotDisplay = protocol === "DMR" ? `TS${rawSlot}` : "--";
let protoColor = "#3b82f6"; if (protocol === "NXDN") protoColor = "#10b981"; else if (protocol === "YSF") protoColor = "#8b5cf6"; else if (protocol === "D-STAR") protoColor = "#06b6d4"; else if (protocol === "P25") protoColor = "#f59e0b";
@@ -953,25 +777,14 @@
async function refreshStats() {
try {
const node = document.getElementById('stat-node-filter').value || 'all';
- const res = await fetch(`/api/stats?node=${node}`);
- const data = await res.json();
-
- // Compila Top TG
- let tgsHtml = data.top_tgs.map((t, i) => `
${i+1}. ${t.target} ${t.count} tx
`).join('');
- document.getElementById('stat-tgs').innerHTML = tgsHtml || t('statNoData');
-
- // Compila Top Callsign
- let callsHtml = data.top_calls.map((c, i) => `
${i+1}. ${c.call} ${c.count} tx
`).join('');
- document.getElementById('stat-calls').innerHTML = callsHtml || t('statNoData');
-
- // Compila Medie e Totali
- document.getElementById('stat-avg').innerText = data.avg_duration;
- document.getElementById('stat-today').innerText = data.today_tx;
-
- } catch (e) { console.error("Errore caricamento statistiche:", e); }
+ const res = await fetch(`/api/stats?node=${node}`); const data = await res.json();
+ document.getElementById('stat-tgs').innerHTML = data.top_tgs.map((t, i) => `
${i+1}. ${t.target} ${t.count} tx
`).join('') || t('statNoData');
+ document.getElementById('stat-calls').innerHTML = data.top_calls.map((c, i) => `
${i+1}. ${c.call} ${c.count} tx
`).join('') || t('statNoData');
+ document.getElementById('stat-avg').innerText = data.avg_duration; document.getElementById('stat-today').innerText = data.today_tx;
+ } catch (e) { console.error("Errore statistiche:", e); }
}
- // --- USER MANAGEMENT (ADD & EDIT) ---
+ // --- USER MANAGEMENT ---
async function openAdmin() { document.getElementById('admin-modal').style.display = 'flex'; loadUsers(); loadSettings(); cancelEdit(); }
function closeAdmin() { document.getElementById('admin-modal').style.display = 'none'; cancelEdit(); }
@@ -979,159 +792,79 @@
const res = await fetch('/api/users'); const users = await res.json();
document.getElementById('users-table-body').innerHTML = users.map(u => `
- | ${u.id} |
- ${u.username} |
- ${u.role.toUpperCase()} |
- ${u.allowed_nodes} |
+ ${u.id} | ${u.username} | ${u.role.toUpperCase()} | ${u.allowed_nodes} |
-
-
+
+
|
`).join('');
}
function startEditUser(id, username, role, nodes) {
- editingUserId = id;
- document.getElementById('new-user').value = username;
- document.getElementById('new-user').disabled = true;
- document.getElementById('new-pass').value = "";
- document.getElementById('new-pass').placeholder = t('phNewPass');
- document.getElementById('new-role').value = role;
- document.getElementById('new-nodes').value = nodes;
-
- const btnSubmit = document.getElementById('btn-user-submit');
- btnSubmit.innerHTML = t('titleSave');
- btnSubmit.style.background = "var(--accent)";
- document.getElementById('btn-user-cancel').style.display = "block";
+ editingUserId = id; document.getElementById('new-user').value = username; document.getElementById('new-user').disabled = true; document.getElementById('new-pass').value = ""; document.getElementById('new-pass').placeholder = t('phNewPass'); document.getElementById('new-role').value = role; document.getElementById('new-nodes').value = nodes;
+ const btnSubmit = document.getElementById('btn-user-submit'); btnSubmit.innerHTML = t('titleSave'); btnSubmit.style.background = "var(--accent)"; document.getElementById('btn-user-cancel').style.display = "block";
}
function cancelEdit() {
- editingUserId = null;
- document.getElementById('new-user').value = "";
- document.getElementById('new-user').disabled = false;
- document.getElementById('new-pass').value = "";
- document.getElementById('new-pass').placeholder = t('phPass');
- document.getElementById('new-role').value = "operator";
- document.getElementById('new-nodes').value = "";
-
- const btnSubmit = document.getElementById('btn-user-submit');
- btnSubmit.innerHTML = t('btnAdd');
- btnSubmit.style.background = "var(--success)";
- document.getElementById('btn-user-cancel').style.display = "none";
+ editingUserId = null; document.getElementById('new-user').value = ""; document.getElementById('new-user').disabled = false; document.getElementById('new-pass').value = ""; document.getElementById('new-pass').placeholder = t('phPass'); document.getElementById('new-role').value = "operator"; document.getElementById('new-nodes').value = "";
+ const btnSubmit = document.getElementById('btn-user-submit'); btnSubmit.innerHTML = t('btnAdd'); btnSubmit.style.background = "var(--success)"; document.getElementById('btn-user-cancel').style.display = "none";
}
async function submitUser() {
- const username = document.getElementById('new-user').value;
- const password = document.getElementById('new-pass').value;
- const role = document.getElementById('new-role').value;
- let allowed = document.getElementById('new-nodes').value;
-
+ const username = document.getElementById('new-user').value; const password = document.getElementById('new-pass').value; const role = document.getElementById('new-role').value; let allowed = document.getElementById('new-nodes').value;
if (!username) return customAlert(t('titleError'), t('msgMissUser'), true);
-
- const payload = { username, role, allowed_nodes: allowed || 'all' };
- if (password) payload.password = password;
+ const payload = { username, role, allowed_nodes: allowed || 'all' }; if (password) payload.password = password;
if (editingUserId) {
- const res = await fetch(`/api/users/${editingUserId}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) });
- const data = await res.json();
+ const res = await fetch(`/api/users/${editingUserId}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); const data = await res.json();
if (data.success) { cancelEdit(); loadUsers(); } else customAlert(t('titleError'), data.error, true);
} else {
if (!password) return customAlert(t('titleError'), t('msgMissPass'), true);
- const res = await fetch('/api/users', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) });
- const data = await res.json();
+ const res = await fetch('/api/users', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); const data = await res.json();
if (data.success) { cancelEdit(); loadUsers(); } else customAlert(t('titleError'), data.error, true);
}
}
- function deleteUser(id) {
- customConfirm(t('titleDelete'), t('msgDelUser'), "var(--danger)", async () => {
- await fetch(`/api/users/${id}`, {method: 'DELETE'});
- loadUsers();
- cancelEdit();
- });
- }
+ function deleteUser(id) { customConfirm(t('titleDelete'), t('msgDelUser'), "var(--danger)", async () => { await fetch(`/api/users/${id}`, {method: 'DELETE'}); loadUsers(); cancelEdit(); }); }
- // --- EMERGENCY & SETTINGS ---
+ // --- SETTINGS ---
function triggerGlobalEmergency() {
- let nameA = "PROFILE A";
- let nameB = "PROFILE B";
- for (const id in globalHealthData) {
- if (globalHealthData[id] && globalHealthData[id].profiles) {
- nameA = globalHealthData[id].profiles.A || nameA;
- nameB = globalHealthData[id].profiles.B || nameB;
- break;
- }
- }
-
- 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';
+ let nameA = "PROFILE A"; let nameB = "PROFILE B";
+ for (const id in globalHealthData) { if (globalHealthData[id] && globalHealthData[id].profiles) { nameA = globalHealthData[id].profiles.A || nameA; nameB = globalHealthData[id].profiles.B || nameB; break; } }
+ 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';
}
function sendGlobalAction(action) {
document.getElementById('override-modal').style.display = 'none';
-
customConfirm(t('titleGlobal'), t('promptOvrConfirm'), "var(--danger)", async () => {
- try {
- const res = await fetch('/api/global_command', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({type: action}) });
- const d = await res.json();
- if(d.success) {
- customAlert(t('titleSuccess'), t('msgOvrOk'));
- } else {
- customAlert(t('titleError'), d.error, true);
- }
- refreshStates();
+ try { const res = await fetch('/api/global_command', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({type: action}) }); const d = await res.json();
+ if(d.success) customAlert(t('titleSuccess'), t('msgOvrOk')); else customAlert(t('titleError'), d.error, true); refreshStates();
} catch(e) { console.error(e); }
});
}
- function changeMyPassword() {
- document.getElementById('new-password-input').value = '';
- document.getElementById('password-modal').style.display = 'flex';
- setTimeout(() => document.getElementById('new-password-input').focus(), 100);
- }
+ function changeMyPassword() { document.getElementById('new-password-input').value = ''; document.getElementById('password-modal').style.display = 'flex'; setTimeout(() => document.getElementById('new-password-input').focus(), 100); }
function closePasswordModal() { document.getElementById('password-modal').style.display = 'none'; }
async function executePasswordChange() {
- const p = document.getElementById('new-password-input').value;
- if (!p) return;
- closePasswordModal();
- try {
- const res = await fetch('/api/change_password', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: sessionStorage.getItem('user_name'), new_password: p}) });
- if ((await res.json()).success) {
- customAlert(t('titleSuccess'), t('msgPassOk'));
- setTimeout(() => logout(), 1500);
- } else {
- customAlert(t('titleError'), t('msgPassErr'), true);
- }
+ const p = document.getElementById('new-password-input').value; if (!p) return; closePasswordModal();
+ try { const res = await fetch('/api/change_password', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: sessionStorage.getItem('user_name'), new_password: p}) });
+ if ((await res.json()).success) { customAlert(t('titleSuccess'), t('msgPassOk')); setTimeout(() => logout(), 1500); } else { customAlert(t('titleError'), t('msgPassErr'), true); }
} catch(e) { console.error(e); }
}
async function loadSettings() { try { const res = await fetch('/api/config'); const data = await res.json(); document.getElementById('update-time-input').value = data.update_schedule; document.getElementById('url-dmr-input').value = data.url_dmr; document.getElementById('url-nxdn-input').value = data.url_nxdn; } catch (e) { console.error(e); } }
-
async function saveSettings() {
const payload = { update_schedule: document.getElementById('update-time-input').value, url_dmr: document.getElementById('url-dmr-input').value, url_nxdn: document.getElementById('url-nxdn-input').value };
- try {
- const res = await fetch('/api/config', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) });
- const data = await res.json();
- if (data.success) customAlert(t('titleSuccess'), t('msgConfigSaved'));
- else customAlert(t('titleError'), t('msgConfigErr'), true);
+ try { const res = await fetch('/api/config', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); const data = await res.json();
+ if (data.success) customAlert(t('titleSuccess'), t('msgConfigSaved')); else customAlert(t('titleError'), t('msgConfigErr'), true);
} catch (e) { console.error(e); }
}
+ // --- SERVICES & CONFIGS ---
function openConfigsModal(clientId) {
- const data = globalHealthData[clientId.toLowerCase()];
- const listDiv = document.getElementById('configs-list');
- let listaFile = data ? (data.config_files || data.files || []) : [];
- if (listaFile.length === 0) {
- listDiv.innerHTML = `
${t('waitData')}
`;
- } else {
- listDiv.innerHTML = listaFile.map(filename => `
-
- ${filename.toUpperCase()}.ini
-
-
`).join('');
- }
+ const data = globalHealthData[clientId.toLowerCase()]; const listDiv = document.getElementById('configs-list'); let listaFile = data ? (data.config_files || data.files || []) : [];
+ if (listaFile.length === 0) listDiv.innerHTML = `
${t('waitData')}
`;
+ else listDiv.innerHTML = listaFile.map(f => `
${f.toUpperCase()}.ini
`).join('');
document.getElementById('configs-modal').style.display = 'flex';
}
function closeConfigsModal() { document.getElementById('configs-modal').style.display = 'none'; }
@@ -1143,29 +876,23 @@
if (!data || !data.processes || Object.keys(data.processes).length === 0) { listDiv.innerHTML = "
No service data available.
"; return; }
let html = "";
for (const [name, status] of Object.entries(data.processes)) {
- const isOnline = status.toLowerCase() === 'online'; const statusColor = isOnline ? 'var(--success)' : 'var(--danger)'; const statusAnim = isOnline ? '' : 'animation: pulse-glow 1.5s infinite;';
- html += `
-
-
- ${name}
- ${status.toUpperCase()}
-
-
- ${!isOnline ? `` : ''}
-
-
- ${isOnline ? `` : ''}
-
-
`;
+ const isOnline = status.toLowerCase() === 'online'; const statusColor = isOnline ? 'var(--success)' : 'var(--danger)';
+ html += `
+
${name}${status.toUpperCase()}
+
+ ${!isOnline ? `` : ''}
+
+
+ ${isOnline ? `` : ''}
+
+
`;
}
listDiv.innerHTML = html;
}
function controlService(clientId, service, action) {
customConfirm(t('titleAction'), `${t('confOp')}
${service}?`, "var(--accent)", async () => {
- try {
- const res = await fetch('/api/service_control', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ clientId, service, action }) });
- const data = await res.json();
+ try { const res = await fetch('/api/service_control', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ clientId, service, action }) }); const data = await res.json();
if(!data.success) customAlert(t('titleError'), "Error: " + data.error, true);
} catch(e) { console.error(e); }
});
@@ -1188,125 +915,44 @@
try {
const res = await fetch('/api/config_file', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ clientId: currentEditClient, service: currentEditService, config_data: { "raw_text": textValue } }) });
const data = await res.json();
- if (data.success) {
- customAlert(t('titleSuccess'), "File updated!");
- closeEditorModal();
- } else {
- customAlert(t('titleError'), "Server error: " + data.error, true);
- statusSpan.innerText = "❌ Error";
- }
- } catch(e) {
- customAlert(t('titleError'), t('msgNetErr'), true);
- statusSpan.innerText = "❌ Network Error";
- }
+ if (data.success) { customAlert(t('titleSuccess'), "File updated!"); closeEditorModal(); } else { customAlert(t('titleError'), "Server error: " + data.error, true); statusSpan.innerText = "❌ Error"; }
+ } catch(e) { customAlert(t('titleError'), t('msgNetErr'), true); statusSpan.innerText = "❌ Network Error"; }
});
}
- // --- FUNZIONE PER ISCRIVERSI ALLE PUSH ---
+ // --- PUSH NOTIFICATIONS ---
async function subscribeToPush() {
if (!('serviceWorker' in navigator)) return;
-
const permission = await Notification.requestPermission();
- if (permission !== 'granted') {
- customAlert("Error", "Push notifications permission denied.", true);
- return;
- }
-
+ if (permission !== 'granted') { customAlert("Error", "Push notifications permission denied.", true); return; }
try {
- const reg = await navigator.serviceWorker.ready;
- const res = await fetch('/api/vapid_public_key');
- const { public_key } = await res.json();
-
- const subscription = await reg.pushManager.subscribe({
- userVisibleOnly: true,
- applicationServerKey: urlB64ToUint8Array(public_key)
- });
-
- await fetch('/api/subscribe', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(subscription)
- });
-
- customAlert("Success", "Push notifications enabled successfully!");
- document.getElementById('push-btn').style.color = 'var(--success)';
- } catch (e) {
- console.error(e);
- customAlert("Error", "Failed to enable notifications.", true);
- }
+ const reg = await navigator.serviceWorker.ready; const res = await fetch('/api/vapid_public_key'); const { public_key } = await res.json();
+ const subscription = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64ToUint8Array(public_key) });
+ await fetch('/api/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) });
+ customAlert("Success", "Push notifications enabled successfully!"); document.getElementById('push-btn').style.color = 'var(--success)';
+ } catch (e) { console.error(e); customAlert("Error", "Failed to enable notifications.", true); }
}
-
- // --- FUNZIONE PER CONTROLLARE IL COLORE DEL BOTTONE ---
async function checkPushStatus() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
-
- try {
- const reg = await navigator.serviceWorker.ready;
- const subscription = await reg.pushManager.getSubscription();
-
- const btn = document.getElementById('push-btn');
- if (subscription && btn) {
- btn.style.color = 'var(--success)';
- }
- } catch(e) {
- console.error("Errore nel controllo stato Push:", e);
- }
+ try { const reg = await navigator.serviceWorker.ready; const subscription = await reg.pushManager.getSubscription(); const btn = document.getElementById('push-btn'); if (subscription && btn) { btn.style.color = 'var(--success)'; } } catch(e) { console.error(e); }
}
+ function urlB64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) outputArray[i] = rawData.charCodeAt(i); return outputArray; }
- // Helper per convertire la chiave VAPID
- function urlB64ToUint8Array(base64String) {
- const padding = '='.repeat((4 - base64String.length % 4) % 4);
- const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
- const rawData = window.atob(base64);
- const outputArray = new Uint8Array(rawData.length);
- for (let i = 0; i < rawData.length; ++i) outputArray[i] = rawData.charCodeAt(i);
- return outputArray;
- }
-
- // AVVIO DELLA DASHBOARD
initUI();
checkPushStatus();
- // --- MOTORE WEBSOCKET REAL-TIME ---
+ // --- WEBSOCKET REAL-TIME ---
const socket = io();
-
- // NUOVO LISTENER PER LO STATO MQTT
socket.on('mqtt_status', function(data) {
const badge = document.getElementById('mqtt-badge');
- 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', () => {
- console.log("🟢 Connesso al server via WebSocket in tempo reale!");
- refreshStates();
- refreshLogs();
- refreshStats();
- });
-
- socket.on('dati_aggiornati', function() {
- console.log("⚡ Rilevato nuovo traffico! Scatto istantaneo dell'interfaccia...");
- refreshStates();
- refreshLogs();
- refreshStats();
+ 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', function() { refreshStates(); refreshLogs(); refreshStats(); });