<!DOCTYPE html><html lang="es" dir="ltr" xmlns="http://www.w3.org/1999/xhtml"><head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <meta name="viewport" content="user-scalable=yes,initial-scale=1.0,minimum-scale=1.0">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="format-detection" content="telephone=no">
    <meta name="robots" content="noindex,nofollow">
    <link rel="manifest" href="{{{domainurl}}}manifest.json">
    <link type="image/x-icon" href="{{{domainurl}}}favicon.ico" rel="shortcut icon">
    <link keeplink="1" type="text/css" href="styles/style-bootstrap.css" media="screen" rel="stylesheet" title="CSS">
    <link type="text/css" href="styles/ol.css" media="screen" rel="stylesheet" title="CSS">
    <link type="text/css" href="styles/ol3-contextmenu.min.css" media="screen" rel="stylesheet" title="CSS">
    <link type="text/css" href="styles/xterm.css" media="screen" rel="stylesheet" title="CSS">
    <link type="text/css" href="styles/flatpickr.min.css" media="screen" rel="stylesheet" title="CSS">
    <link id="theme-stylesheet" href="styles/bootstrap{{{min}}}.css" rel="stylesheet" title="CSS">
    <link href="styles/select2.min.css" rel="stylesheet" title="CSS">
    <link href="styles/select2-bootstrap-5-theme.min.css" rel="stylesheet" title="CSS">
    </head><body id="body" oncontextmenu="handleContextMenu(event)" style="display:none;min-width:495px" onload="if (typeof(startup) !== 'undefined') startup();">{{{customCSSTags}}}
    <link rel="apple-touch-icon" href="/favicon-303x303.png">
    <script type="text/javascript" src="scripts/common-0.0.1{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/meshcentral{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/amt-0.2.0{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/amt-wsman-0.2.0{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/amt-desktop-0.0.2{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/amt-terminal-0.0.2{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/zlib{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/zlib-inflate{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/zlib-adler32{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/zlib-crc32{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/amt-redir-ws-0.1.0{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/amt-wsman-ws-0.2.0{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/agent-redir-ws-0.1.1{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/agent-redir-rtc-0.1.0{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/agent-desktop-0.0.2{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/agent-rdp-0.0.1{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/qrcode.min.js"></script>
    <script type="text/javascript" src="scripts/xterm{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/xterm-addon-fit{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/xterm-addon-image{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/flatpickr.js"></script>
    <script type="text/javascript" src="mstsc/mstsc.js"></script>
    <script type="text/javascript" src="mstsc/keyboard.js"></script>
    <script type="text/javascript" src="mstsc/rle.js"></script>
    <script type="text/javascript" src="mstsc/client.js"></script>
    <script type="text/javascript" src="mstsc/canvas.js"></script>
    <script type="text/javascript" src="scripts/jquery{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/bootstrap{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/fontawesome/all{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/select2.full.min.js"></script>
    <script type="text/javascript" src="scripts/themes/theme-switcher.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/u2f-api{{{min}}}.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/charts{{{min}}}.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/moment{{min}}.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/chartjs-adapter-moment{{{min}}}.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/filesaver.min.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/ol{{{min}}}.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/ol3-contextmenu{{{min}}}.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/purify{{{min}}}.js"></script>
    <script keeplink="1" type="text/javascript" src="scripts/marked{{{min}}}.js"></script>
    <script type="text/javascript" src="js/ui-components.js"></script>
    {{{customJSTags}}}
    <title>{{{title}}}</title>



    <script>
        if (('ontouchstart' in window || navigator.maxTouchPoints > 0) && window.matchMedia('(pointer: coarse)').matches) {
            document.body.classList.add('is-mobile');
            document.documentElement.classList.add('is-mobile');
        }
        // Auto-fullscreen on landscape: when in the desktop view and connected,
        // rotating to landscape triggers deskToggleFull() so the existing fulldesk
        // CSS maximises the canvas without any new layout code.
        // A flag tracks whether WE triggered fullscreen so we can auto-exit on
        // rotating back to portrait (but not if the user exited fullscreen manually).
        var _mobileAutoFullscreen = false;
        var _mobileSavedAspectRatio = 0;
        var _mobileZoomMode = 'fit'; // 'native' = 1:1 scrollable  |  'fit' = fill viewport
        var _nativePinchStartDist = 0, _nativePinchStartW = 0, _nativePinchStartH = 0;
        var _nativePinchMidX = 0, _nativePinchMidY = 0;
        var _prevHadFulldesk  = false;

        // Switch between native 1:1 (scrollable, full precision) and fit-to-viewport.
        // In native mode we suspend desktop.m.onScreenSizeChange (= deskAdjust) so
        // it can't override the explicit pixel dimensions we set on the canvas.
        function _mobileSetZoom(mode) {
            _mobileZoomMode = mode;
            var desk = document.getElementById('Desk');
            var dp   = document.getElementById('DeskParent');
            if (!desk || !dp) return;
            var dm = (typeof desktop !== 'undefined') && desktop && desktop.m;
            if (mode === 'native') {
                // CSS custom properties lock canvas at native resolution.
                // Panning is handled by dtouchmove JS (scrollLeft/Top on deskarea3x).
                // Touch listeners stay active so trackpad mode keeps working.
                var w = (dm && dm.ScreenWidth)  || desk.width  || 640;
                var h = (dm && dm.ScreenHeight) || desk.height || 480;
                document.documentElement.style.setProperty('--native-desk-w', w + 'px');
                document.documentElement.style.setProperty('--native-desk-h', h + 'px');
                document.body.classList.add('native-zoom');
                if (dm && !dm._savedOnScreenSizeChange) {
                    dm._savedOnScreenSizeChange = dm.onScreenSizeChange;
                    dm.onScreenSizeChange = function(obj, sw, sh) {
                        document.documentElement.style.setProperty('--native-desk-w', sw + 'px');
                        document.documentElement.style.setProperty('--native-desk-h', sh + 'px');
                    };
                }
            } else {
                document.body.classList.remove('native-zoom');
                if (dm && dm._savedOnScreenSizeChange) {
                    dm.onScreenSizeChange = dm._savedOnScreenSizeChange;
                    delete dm._savedOnScreenSizeChange;
                }
                desk.style.width  = '100%';
                desk.style.height = '100%';
                if (typeof deskAdjust === 'function') deskAdjust();
            }
        }

        // Watch body class for fulldesk being added/removed and apply the right zoom
        if (document.body.classList.contains('is-mobile')) {
            new MutationObserver(function(muts) {
                muts.forEach(function(m) {
                    if (m.attributeName !== 'class') return;
                    var inFull = document.body.classList.contains('fulldesk');
                    if (inFull && !_prevHadFulldesk) {
                        // Just entered fullscreen: portrait->native, landscape->fit
                        setTimeout(function() {
                            var landscape = window.matchMedia('(orientation: landscape)').matches;
                            _mobileSetZoom(landscape ? 'fit' : 'native');
                        }, 200);
                    } else if (!inFull && _prevHadFulldesk) {
                        _mobileSetZoom('fit'); // always reset to fit on exit
                    }
                    _prevHadFulldesk = inFull;
                });
            }).observe(document.body, { attributes: true });
        }
        function _doOrientationUpdate() {
            if (!document.body.classList.contains('is-mobile')) return;
            var landscape = window.matchMedia('(orientation: landscape)').matches;
            document.body.classList.toggle('is-landscape', landscape);
            // Check DOM state — JS variables like xxcurrentView / fullscreen are
            // in a later function scope and not accessible from this early script.
            var p11 = document.getElementById('p11');
            var inDesktop = p11 && p11.style.display !== 'none';
            var alreadyFullscreen = document.body.classList.contains('fulldesk');
            if (landscape && inDesktop && !alreadyFullscreen) {
                // Auto-enter fullscreen in landscape using the global function
                if (typeof deskToggleFull === 'function') {
                    _mobileAutoFullscreen = true;
                    _mobileSavedAspectRatio = (typeof deskAspectRatio !== 'undefined') ? deskAspectRatio : 0;
                    deskToggleFull();
                }
                // CSS rule body.is-mobile.is-landscape.fulldesk #Desk { width/height: 100% !important }
                // handles the canvas fill automatically — no JS override needed here.
            } else if (!landscape && alreadyFullscreen && _mobileAutoFullscreen) {
                _mobileAutoFullscreen = false;
                if (typeof deskToggleFull === 'function') deskToggleFull();
                // Let deskAdjust() recalculate naturally for portrait (no forced CSS override)
                setTimeout(function() { if (typeof deskAdjust === 'function') deskAdjust(); }, 400);
            } else {
                // Not entering/exiting fullscreen but still call deskAdjust so the
                // canvas recalculates for the new viewport dimensions
                if (typeof deskAdjust === 'function') {
                    requestAnimationFrame(function() { deskAdjust(); });
                }
            }
        }
        function _updateOrientation() {
            setTimeout(_doOrientationUpdate, 300);
        }
        window.addEventListener('orientationchange', _updateOrientation);
        window.addEventListener('resize', _updateOrientation);

        // Use Popper strategy:'fixed' so dropdown menus escape overflow:hidden parents and canvas layers.
        window.addEventListener('load', function() {
            if (typeof bootstrap === 'undefined') return;
            document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(function(el) {
                var ex = bootstrap.Dropdown.getInstance(el);
                if (ex) ex.dispose();
                new bootstrap.Dropdown(el, {
                    popperConfig: function(cfg) { cfg.strategy = 'fixed'; return cfg; }
                });
            });
        });

        function mobileSubMenuScroll(tableId, dir) {
            if (!document.body.classList.contains('is-mobile')) return;
            var inner = document.getElementById(tableId + 'ScrollInner');
            if (!inner) return;
            inner.scrollLeft += dir * 80;
        }
    </script>
    <!-- right click menu -->
    <div id="contextMenu" class="contextMenu noselect" style="display:none">
        <div id="cxinfo" class="cmtext" onclick="cmaction(1,event)"><b>Información</b></div>
        <div id="cxdesktop" class="cmtext" onclick="cmaction(3,event)">Escritorio</div>
        <div id="cxterminal" class="cmtext" onclick="cmaction(2,event)">Terminal</div>
        <div id="cxfiles" class="cmtext" onclick="cmaction(4,event)">Archivos</div>
        <div id="cxevents" class="cmtext" onclick="cmaction(5,event)">Eventos</div>
        <div id="cxdetails" class="cmtext" onclick="cmaction(6,event)">Detalles</div>
        <div id="cxconsole" class="cmtext" onclick="cmaction(7,event)">Consola</div>
        <div id="cxwebrdp" class="cmtext" onclick="cmaction(11,event)">Web-RDP</div>
        <div id="cxwebvnc" class="cmtext" onclick="cmaction(12,event)">Web-VNC</div>
        <div id="cxwebssh" class="cmtext" onclick="cmaction(13,event)">Web-SSH</div>
        <div id="cxrdp" class="cmtext" onclick="cmaction(14,event)">RDP</div>
        <div id="cxplugins" class="cmtext" onclick="cmaction(8,event)" style="display:none">Plugins</div>
        <hr id="cxmgroupsplit">
        <div id="cxstar" class="cmtext" onclick="cmaction(10,event)" style="display:none">Alternar Favorito</div>
        <div id="cxmdesktop" class="cmtext" onclick="cmaction(9,event)" style="display:none">Multi-escritorio</div>
    </div>
    <div id="meshContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div id="cxselectall" class="cmtext" onclick="cmmeshaction(1,event)">Seleccionar Todo</div>
        <div id="cxselectnone" class="cmtext" onclick="cmmeshaction(2,event)">No seleccionar nada</div>
        <!--
        <hr id="cxmgroupsplit2" style="display:none" />
        <div id="cxmmdesktop" class="cmtext" style="display:none" onclick="cmmeshaction(3,event)">Multi-Desktop</div>
        -->
    </div>
    <div id="altPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmaltportaction(1,event)">Alternar Puerto</div>
    </div>
    <div id="sshPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmsshportaction(1,event)">Alternar Puerto</div>
    </div>
    <div id="rfbPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmrfbportaction(1,event)">Alternar Puerto</div>
    </div>
    <div id="httpPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpportaction(1,event)">Alternar Puerto</div>
    </div>
    <div id="httpsPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpsportaction(1,event)">Alternar Puerto</div>
    </div>
    <div id="filesContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmfilesaction(1,event)">Renombrar</div>
        <div class="cmtext" onclick="cmfilesaction(2,event)">Editar</div>
        <div class="cmtext" onclick="cmfilesaction(3,event)">Eliminar</div>
    </div>
    <div id="expandAllContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmexpandaction(1,event)">Expandir Todo</div>
        <div class="cmtext" onclick="cmexpandaction(2,event)">Desplegar todo</div>
    </div>
    <div id="deskPlayerContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskplayeraction(1,event)">Abrir Reproductor...</div>
    </div>
    <div id="deskKeyShortcutContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskshortcutaction(1,event)">Personalizar ...</div>
    </div>
    <div id="deskPreConfigShortcutContextMenu" class="contextMenu noselect" style="display:none;min-width:0px;max-height:calc(80% - 50px);overflow-y:auto">
        <span id="deskPreConfigShortcutContextMenu1"></span>
        <span id="deskPreConfigShortcutContextMenu2"></span>
        <div class="cmtext" onclick="cmdeskpreconfigtypeaction(-1,event)">Personalizar ...</div>
    </div>
    <div id="deskPreConfigScriptContextMenu" class="contextMenu noselect" style="display:none;min-width:0px;max-height:calc(80% - 50px);overflow-y:auto">
        <span id="deskPreConfigScriptContextMenu1"></span>
        <span id="deskPreConfigScriptContextMenu2"></span>
    </div>
    <!--
    <div id="pluginTabContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div id="cxclose" class="cmtext" onclick="pluginTabClose(event)">Close Tab</div>
    </div>
    -->
    <!-- main page -->
    <div id="container">
        <div id="notifiyBox" class="notifiyBox" style="display:none"></div>
        <div id="masthead" class="noselect masthead-container">
            <div class="masthead-left">
                <div class="masthead-title" onclick="go(1,event)">{{{titlehtml}}}</div>
                <div class="masthead-titles-container">
                    <div class="title" onclick="go(1,event)">{{{title1}}}</div>
                    <div class="title2" onclick="go(1,event)">{{{title2}}}</div>
                </div>
            </div>

            <div class="masthead-right">
                <div id="notificationCount" class="notification-icon position-relative" title="Haz clic para ver las notificaciones actuales" onclick="clickNotificationIcon()" style="display: none;">
                    <i class="fa-solid fa-bell fa-xl text-white"></i>
                    <span id="notificationBadge" class="position-absolute top-1 start-100 translate-middle badge rounded-pill bg-danger">0</span>
                </div>

                <div class="user-menu-container">
                    <div id="userDropdown">
                        <div id="userDropdownButton">
                            <img id="userDropdownImage" src="images/user-256.png" alt="User">
                            <span id="userDropdownName" class="LogoffLinkColor"></span>
                            <i class="fa fa-chevron-down LogoffLinkColor" style="font-size: 12px;"></i>
                        </div>
                        <div id="userDropdownMenu">
                            <div id="userDropdownMenuContainer">
                                <div class="userDropdownMenuItem userDropdownMobileOnly mobile-menu-item" data-action="devices" onclick="handleUserMenuItem('devices')">
                                    <i class="fa fa-desktop userDropdownMenuIcon"></i>
                                    <span>Mis Dispositivos</span>
                                </div>

                                <div class="userDropdownMenuItem userDropdownMobileOnly mobile-menu-item" data-action="events" onclick="handleUserMenuItem('events')">
                                    <i class="fa fa-calendar userDropdownMenuIcon"></i>
                                    <span>Mis Eventos</span>
                                </div>

                                <div id="mobileUsersMenuItem" class="userDropdownMenuItem userDropdownMobileOnly users-menu-item mobile-menu-item d-none" data-action="users" onclick="handleUserMenuItem('users')">
                                    <i class="fa fa-users userDropdownMenuIcon"></i>
                                    <span>Mis Usuarios</span>
                                </div>

                                <div id="mobileFilesMenuItem" class="userDropdownMenuItem userDropdownMobileOnly files-menu-item mobile-menu-item d-none" data-action="files" onclick="handleUserMenuItem('files')">
                                    <i class="fa fa-folder userDropdownMenuIcon"></i>
                                    <span>Mis Archivos</span>
                                </div>

                                <div id="mobileServerMenuItem" class="userDropdownMenuItem userDropdownMobileOnly server-menu-item mobile-menu-item d-none" data-action="server" onclick="handleUserMenuItem('server')">
                                    <i class="fa fa-server userDropdownMenuIcon"></i>
                                    <span>Mi Servidor</span>
                                </div>

                                <div id="userDropdownMenuDivider" class="userDropdownMobileOnly mobile-menu-item"></div>

                                <div class="userDropdownMenuItem userDropdownUISettings" onclick="toggleUISubmenu(); event.stopPropagation();">
                                    <i class="fa fa-sliders userDropdownMenuIcon"></i>
                                    <span>Configuración de la interfaz de usuario</span>
                                    <i class="fa fa-chevron-right userDropdownMenuIcon submenu-arrow"></i>
                                </div>

                                <div id="uiSubmenu" class="userDropdownSubmenu">
                                    <div id="toggleModernUIMenuItem" class="userDropdownMenuItem" data-action="toggle-modern-ui" onclick="handleUserMenuItem('toggle-modern-ui')">
                                        <i class="fa fa-toggle-on userDropdownMenuIcon"></i>
                                        <span>Alternar interfaz de usuario moderna</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="left-bar" onclick="handleUserMenuItem('left-bar')">
                                        <i class="fa fa-window-maximize fa-rotate-270 userDropdownMenuIcon"></i>
                                        <span>Interfaz de barra izquierda</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="top-bar" onclick="handleUserMenuItem('top-bar')">
                                        <i class="fa fa-window-maximize userDropdownMenuIcon"></i>
                                        <span>Interfaz de barra superior</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="fixed-width" onclick="handleUserMenuItem('fixed-width')">
                                        <i class="fa fa-columns userDropdownMenuIcon"></i>
                                        <span>Interfaz de ancho fijo</span>
                                    </div>
                                    <div id="toggleFooterMenuItem" class="userDropdownMenuItem" data-action="toggle-footer" onclick="handleUserMenuItem('toggle-footer')">
                                        <i class="fa fa-window-minimize userDropdownMenuIcon"></i>
                                        <span>Alternar barra de pie de página</span>
                                    </div>
                                    <div id="userDropdownMenuDivider" class="userDropdownMobileOnly"></div>
                                </div>

                                <div id="toggleNightMenuItem" class="userDropdownMenuItem" data-action="toggle-night" onclick="toggleNightMode(); QV('userDropdownMenu', false); QV('uiSubmenu', false); resetChevronArrow();">
                                    <i id="nightModeIcon" class="fa userDropdownMenuIcon night-mode-icon"></i>
                                    <span id="nightModeText">Alternar modo nocturno</span>
                                </div>


                                <div class="userDropdownMenuItem" data-action="notes" onclick="handleUserMenuItem('notes')">
                                    <i class="fa fa-sticky-note userDropdownMenuIcon"></i>
                                    <span>Notas Personales</span>
                                </div>

                                <div class="userDropdownMenuItem" data-action="account" onclick="handleUserMenuItem('account')">
                                    <i class="fa fa-user userDropdownMenuIcon"></i>
                                    <span>Mi Cuenta</span>
                                </div>

                                <div id="userDropdownMenuDivider"></div>

                                <div class="userDropdownMenuItem" data-action="logout" onclick="handleUserMenuItem('logout')">
                                    <i class="fa fa-sign-out userDropdownMenuIcon"></i>
                                    <span>Cerrar sesión</span>
                                </div>
                            </div>
                        </div>
                    </div>
                    <span id="idleTimeoutNotify" class="idle-timeout-notify"></span>
                </div>
            </div>
        </div>
        <div class="sidebar flex-column" id="page_leftbar">
            <div style="height:24px"></div>
            <nav class="nav flex-column">
                <a class="nav-link active text-center text-white lbbuttonsel" id="LeftMenuMyDevices" href="#" data-target="general" onclick="go(1,event)" onkeypress="if (event.key=='Enter') { go(1); }"><i class="fa-solid fa-computer me-2"></i></a>
                <a class="nav-link text-center text-white" id="LeftMenuMyAccount" href="#" data-target="account" onclick="go(2,event)" onkeypress="if (event.key=='Enter') { go(2); }"><i class="fa-solid fa-user-gear me-2"></i></a>
                <a class="nav-link text-center text-white" id="LeftMenuMyEvents" href="#" data-target="events" onclick="go(3,event)" onkeypress="if (event.key=='Enter') { go(3); }"><i class="fa-solid fa-calendar-alt me-2"></i></a>
                <a class="nav-link text-center text-white" id="LeftMenuMyFiles" href="#" data-target="files" onclick="go(5,event)" onkeypress="if (event.key=='Enter') { go(5); }"><i class="fa-solid fa-folder-open me-2"></i></a>
                <a class="nav-link text-center text-white" id="LeftMenuMyUsers" href="#" data-target="users" onclick="go(4,event)" onkeypress="if (event.key=='Enter') { go(4); }"><i class="fa-solid fa-users me-2"></i></a>
                <a class="nav-link text-center text-white" id="LeftMenuMyServer" href="#" data-target="server" onclick="go(6,event)" onkeypress="if (event.key=='Enter') { go(6); }"><i class="fa-solid fa-server me-2"></i></a>
            </nav>
        </div>
        <div id="topbar" class="noselect">
            <div>
                <div style="position:relative">
                    <span id="logoutControlSpan2"></span>
                    <table id="MainMenuSpan" cellpadding="0" cellspacing="0" class="style1">
                        <tbody><tr>
                            <td tabindex="0" id="MainMenuMyDevices" class="topbar_td style3x" onmouseup="go(1,event)" onkeypress="if (event.key == 'Enter') go(1)">Mis Dispositivos</td>
                            <td tabindex="0" id="MainMenuMyAccount" class="topbar_td style3x" onmouseup="go(2,event)" onkeypress="if (event.key == 'Enter') go(2)">Mi Cuenta</td>
                            <td tabindex="0" id="MainMenuMyEvents" class="topbar_td style3x" onmouseup="go(3,event)" onkeypress="if (event.key == 'Enter') go(3)">Mis Eventos</td>
                            <td tabindex="0" id="MainMenuMyFiles" class="topbar_td style3x" onmouseup="go(5,event)" onkeypress="if (event.key == 'Enter') go(5)">Mis Archivos</td>
                            <td tabindex="0" id="MainMenuMyUsers" class="topbar_td style3x" onmouseup="go(4,event)" onkeypress="if (event.key == 'Enter') go(4)">Mis Usuarios</td>
                            <td tabindex="0" id="MainMenuMyServer" class="topbar_td style3x" onmouseup="go(6,event)" onkeypress="if (event.key == 'Enter') go(6)">Mi Servidor</td>
                            <td class="topbar_td_end style3">&nbsp;</td>
                        </tr>
                    </tbody></table>
                    <div id="MainSubMenuSpan" style="display:none">
                        <div class="mobile-submenu-scroll">
                            <button class="mobile-submenu-arrow" onclick="mobileSubMenuScroll('MainSubMenu',-1)" aria-label="Scroll left"><i class="fa fa-caret-square-left"></i></button>
                            <div class="mobile-submenu-scroll-inner" id="MainSubMenuScrollInner">
                                <table id="MainSubMenu" cellpadding="0" cellspacing="0" class="style1">
                                    <tbody><tr>
                                        <td tabindex="0" id="MainDev" class="topbar_td style3x" onmouseup="go(10,event)" onkeypress="if (event.key == 'Enter') go(10)">General</td>
                                        <td tabindex="0" id="MainDevDesktop" class="topbar_td style3x" onmouseup="go(11,event)" onkeypress="if (event.key == 'Enter') go(11)">Escritorio</td>
                                        <td tabindex="0" id="MainDevTerminal" class="topbar_td style3x" onmouseup="go(12,event)" onkeypress="if (event.key == 'Enter') go(12)">Terminal</td>
                                        <td tabindex="0" id="MainDevFiles" class="topbar_td style3x" onmouseup="go(13,event)" onkeypress="if (event.key == 'Enter') go(13)">Archivos</td>
                                        <td tabindex="0" id="MainDevEvents" class="topbar_td style3x" onmouseup="go(16,event)" onkeypress="if (event.key == 'Enter') go(16)">Eventos</td>
                                        <td tabindex="0" id="MainDevInfo" class="topbar_td style3x" onmouseup="go(17,event)" onkeypress="if (event.key == 'Enter') go(17)">Detalles</td>
                                        <td tabindex="0" id="MainDevApps" class="topbar_td style3x" onmouseup="go(18,event)" onkeypress="if (event.key == 'Enter') go(18)">Software</td>
                                        <td tabindex="0" id="MainDevAmt" class="topbar_td style3x" onmouseup="go(14,event)" onkeypress="if (event.key == 'Enter') go(14)">Intel®AMT</td>
                                        <td tabindex="0" id="MainDevConsole" class="topbar_td style3x" onmouseup="go(15,event)" onkeypress="if (event.key == 'Enter') go(15)">Consola</td>
                                        <td tabindex="0" id="MainDevPlugins" class="topbar_td style3x" onmouseup="go(19,event)" onkeypress="if (event.key == 'Enter') go(19)">Plugins</td>
                                        <td class="topbar_td_end style3">&nbsp;</td>
                                    </tr>
                                </tbody></table>
                            </div>
                            <button class="mobile-submenu-arrow" onclick="mobileSubMenuScroll('MainSubMenu',1)" aria-label="Scroll right"><i class="fa fa-caret-square-right"></i></button>
                        </div>
                    </div>
                    <div id="MeshSubMenuSpan" style="display:none">
                        <table id="MeshSubMenu" cellpadding="0" cellspacing="0" class="style1">
                            <tbody><tr>
                                <td tabindex="0" id="MeshGeneral" class="topbar_td style3x" onmouseup="go(20,event)" onkeypress="if (event.key == 'Enter') go(20)">General</td>
                                <td tabindex="0" id="MeshSummary" class="topbar_td style3x" onmouseup="go(21,event)" onkeypress="if (event.key == 'Enter') go(21)">Resumen</td>
                                <td class="topbar_td_end style3">&nbsp;</td>
                            </tr>
                        </tbody></table>
                    </div>
                    <div id="EventsSubMenuSpan" style="display:none">
                        <table id="EventsSubMenu" cellpadding="0" cellspacing="0" class="style1">
                            <tbody><tr>
                                <td tabindex="0" id="EventsLive" class="topbar_td style3x" onmouseup="go(3,event)" onkeypress="if (event.key == 'Enter') go(3)">Eventos</td>
                                <td tabindex="0" id="EventsReport" class="topbar_td style3x" onmouseup="go(60,event)" onkeypress="if (event.key == 'Enter') go(60)">Informes</td>
                                <td class="topbar_td_end style3">&nbsp;</td>
                            </tr>
                        </tbody></table>
                    </div>
                    <div id="UserSubMenuSpan" style="display:none">
                        <table id="UserSubMenu" cellpadding="0" cellspacing="0" class="style1">
                            <tbody><tr>
                                <td tabindex="0" id="UserGeneral" class="topbar_td style3x" onmouseup="go(30,event)" onkeypress="if (event.key == 'Enter') go(30)">General</td>
                                <td tabindex="0" id="UserEvents" class="topbar_td style3x" onmouseup="go(31,event)" onkeypress="if (event.key == 'Enter') go(31)">Eventos</td>
                                <td class="topbar_td_end style3">&nbsp;</td>
                            </tr>
                        </tbody></table>
                    </div>
                    <div id="UsersSubMenuSpan" style="display:none">
                        <table id="UsersSubMenu" cellpadding="0" cellspacing="0" class="style1">
                            <tbody><tr>
                                <td tabindex="0" id="UsersGeneral" class="topbar_td style3x" onmouseup="go(4,event)" onkeypress="if (event.key == 'Enter') go(4)">Usuarios</td>
                                <td tabindex="0" id="UsersGroups" class="topbar_td style3x" onmouseup="go(50,event)" onkeypress="if (event.key == 'Enter') go(50)">Grupos</td>
                                <td tabindex="0" id="UsersRecordings" class="topbar_td style3x" onmouseup="go(52,event)" onkeypress="if (event.key == 'Enter') go(52)">Grabaciones</td>
                                <td class="topbar_td_end style3">&nbsp;</td>
                            </tr>
                        </tbody></table>
                    </div>
                    <div id="ServerSubMenuSpan" style="display:none">
                        <table id="ServerSubMenu" cellpadding="0" cellspacing="0" class="style1">
                            <tbody><tr>
                                <td tabindex="0" id="ServerGeneral" class="topbar_td style3x" onmouseup="go(6,event)" onkeypress="if (event.key == 'Enter') go(6)">General</td>
                                <td tabindex="0" id="ServerStats" class="topbar_td style3x" onmouseup="go(40,event)" onkeypress="if (event.key == 'Enter') go(40)">Estadísticas</td>
                                <td tabindex="0" id="ServerConsole" class="topbar_td style3x" onmouseup="go(115,event)" onkeypress="if (event.key == 'Enter') go(115)">Consola</td>
                                <td tabindex="0" id="ServerTrace" class="topbar_td style3x" onmouseup="go(41,event)" onkeypress="if (event.key == 'Enter') go(41)">Seguimiento</td>
                                <td tabindex="0" id="ServerPlugins" class="topbar_td style3x" onmouseup="go(42,event)" onkeypress="if (event.key == 'Enter') go(42)">Plugins</td>
                                <td class="topbar_td_end style3">&nbsp;</td>
                            </tr>
                        </tbody></table>
                    </div>
                </div>
            </div>
        </div>
        <div id="column_l" class="pt-3">
            <div id="p0" style="display:none">
                <div id="p0message"><span id="p0span">Servidor desconectado</span>, <href onclick="reload()" style="cursor:pointer"><u>haz clic para reconectar</u></href>.</div>
            </div>
            <div id="p1" style="display:none">
                <div id="p1title" class="d-flex justify-content-between align-items-center">
                    <div class="fs-4 fw-bold">Mis Dispositivos</div>
                    <div style="display:none" id="devListToolbarViewIcons">
                        <div id="devViewPageState" style="float:left;line-height:32px;height:32px;font-size:16px;display:none"></div>
                        <div tabindex="0" id="devViewPageButton1" class="viewSelector" onclick="onDeviceViewPageChange(1)" onkeypress="if (event.key == 'Enter') { onDeviceViewPageChange(1); }" title="Primera pagina" style="display:none">
                            <div class="viewSelector9"></div>
                        </div>
                        <div tabindex="0" id="devViewPageButton2" class="viewSelector" onclick="onDeviceViewPageChange(2)" onkeypress="if (event.key == 'Enter') { onDeviceViewPageChange(2); }" title="Pagina anterior" style="display:none">
                            <div class="viewSelector10"></div>
                        </div>
                        <div tabindex="0" id="devViewPageButton3" class="viewSelector" onclick="onDeviceViewPageChange(3)" onkeypress="if (event.key == 'Enter') { onDeviceViewPageChange(3); }" title="Siguiente Pagina" style="display:none">
                            <div class="viewSelector11"></div>
                        </div>
                        <div tabindex="0" id="devViewPageButton4" class="viewSelector" onclick="onDeviceViewPageChange(4)" onkeypress="if (event.key == 'Enter') { onDeviceViewPageChange(4); }" title="Ultima Pagina" style="display:none;margin-right:12px">
                            <div class="viewSelector8"></div>
                        </div>
                        <span role="button" tabindex="0" id="devViewButton1" class="fa-layers fa-fw fa-xl" onclick="onDeviceViewChange(1)" onkeypress="if (event.key == 'Enter') { onDeviceViewChange(1); }" title="Columnas">
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 right-4 up-2"></i>
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 left-4 up-2"></i>
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 right-4 down-3"></i>
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 left-4 down-3"></i>
                        </span>
                        <span role="button" tabindex="0" id="devViewButton2" class="fa-layers fa-fw fa-xl" onclick="onDeviceViewChange(2)" onkeypress="if (event.key == 'Enter') { onDeviceViewChange(2); }" title="Lista">
                            <i class="fa-solid fa-bars"></i>
                        </span>
                        <span role="button" tabindex="0" id="devViewButton3" class="fa-layers fa-fw fa-xl" onclick="onDeviceViewChange(3)" onkeypress="if (event.key == 'Enter') { onDeviceViewChange(3); }" title="Escritorios">
                            <i class="fa-solid fa-square" data-fa-transform="shrink-9 up-4 left-4"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-9 up-4 right-4"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-9 down-4 left-4"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-9 down-4 right-4"></i>
                        </span>
                        <span role="button" tabindex="0" id="devViewButton5" class="fa-layers fa-fw fa-xl" onclick="onDeviceViewChange(5)" onkeypress="if (event.key == 'Enter') { onDeviceViewChange(5); }" title="Escritorios">
                            <i class="fa-solid fa-square" data-fa-transform="shrink-10 up-4 left-6"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-10 up-4 right-1"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-10 up-4 right-5"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-10 down-4 left-1"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-10 down-4 left-5"></i>
                            <i class="fa-solid fa-square" data-fa-transform="shrink-10 down-4 right-6"></i>
                        </span>
                        <span role="button" tabindex="0" id="devViewButton4" class="fa-layers fa-fw fa-xl" onclick="onDeviceViewChange(4)" onkeypress="if (event.key == 'Enter') { onDeviceViewChange(4); }" title="Mapa" style="display:none">
                            <i class="fa-solid fa-map-location-dot"></i>
                        </span>
                    </div>
                </div>
                <div id="devListToolbarSpan" class="noselect d-flex align-items-center p-1">
                    <div id="devListToolbar" class="d-flex align-items-center visually-hidden">
                        <button class="btn btn-secondary btn-sm fa-layers fa-fw me-1" style="width: 30px; height: 31px; display:none" title="Desplegar todo" id="CollapseAllButton" onclick="cmexpandaction(2)">
                            <i class="fa-solid fa-chevron-up" data-fa-transform="down-6"></i>
                            <i class="fa-solid fa-chevron-down" data-fa-transform="up-6"></i>
                        </button>
                        <button class="btn btn-secondary btn-sm fa-layers fa-fw me-1" style="width: 30px; height: 31px; display:none" title="Expandir Todo" id="ExpandAllButton" onclick="cmexpandaction(1)">
                            <i class="fa-solid fa-chevron-up" data-fa-transform="up-6"></i>
                            <i class="fa-solid fa-chevron-down" data-fa-transform="down-6"></i>
                        </button>
                        <input type="button" class="btn btn-secondary me-1 btn-sm" id="SelectAllButton" onclick="selectallButtonFunction();" value="Seleccionar Todo">
                        <input type="button" class="btn btn-primary me-1 btn-sm" id="GroupActionButton" disabled="disabled" value="Acción de Grupo" onclick="groupActionFunction()">
                        <input type="button" class="btn btn-secondary me-1 btn-sm" id="ScrollToTopButton" onclick="onDevicesScroll(true);" value="Scroll To Top">
                        <input type="text" id="SearchInput" class="form-control-sm me-1 btn-sm" placeholder="Filtrar" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Filtrar dispositivos por nombre o prefijo: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Prefijo ! para negar, p.ej. !tag:abc">
                        <span id="SearchInputClearButton" style="display:none;position:relative">
                            <span class="fa-layers fa-fw" role="button" onclick="clearDeviceSearch()" style="position:absolute;left:-25px;top:-15px">
                                <i class="fa-solid fa-circle" style="color:var(--bs-secondary-bg)"></i>
                                <i class="fa-solid fa-xmark" style="color:var(--bs-body-color)"></i>
                            </span>
                        </span>
                        <select class="form-select-sm me-1" id="DevFilterSelect" onchange="onOnlineCheckBox(event)" title="Filtro de dispositivo">
                            <option value="0" selected="">Todos</option>
                            <option value="1">En línea</option>
                            <option value="5">Fuera de línea</option>
                            <option value="2">Sesiones</option>
                            <option value="3">Destacado</option>
                            <option value="4">Intel® AMT</option>
                            <option value="6">Ayuda</option>
                            <option value="7">Etiquetado</option>
                            <option value="8">Sin etiquetar</option>
                        </select>
                        <div class="form-check" title="Mostrar el nombre del sistema operativo de los dispositivos"><input class="form-check-input me-1" type="checkbox" id="RealNameCheckBox" onclick="onRealNameCheckBox()"><label class="form-check-label" for="RealNameCheckBox">Nombre del Sistema Operativo</label></div>
                        <label style="display:none"><input type="checkbox" id="OnlineCheckBox" class="form-check-input me-2" onclick="onOnlineCheckBox(event)"><span title="Solo mostrar dispositivos que están en línea">En línea</span></label>
                        <span id="devsCustomUIBar"></span>
                    </div>
                    <div id="kvmListToolbar" class="d-flex align-items-center style14 visually-hidden">
                        <button class="btn btn-secondary btn-sm fa-layers fa-fw me-1" style="width: 30px; height: 31px; display:none" title="Desplegar todo" id="CollapseAllButton2" onclick="cmexpandaction(2)">
                            <i class="fa-solid fa-chevron-up" data-fa-transform="down-6"></i>
                            <i class="fa-solid fa-chevron-down" data-fa-transform="up-6"></i>
                        </button>
                        <button class="btn btn-secondary btn-sm fa-layers fa-fw me-1" style="width: 30px; height: 31px; display:none" title="Expandir Todo" id="ExpandAllButton2" onclick="cmexpandaction(1)">
                            <i class="fa-solid fa-chevron-up" data-fa-transform="up-6"></i>
                            <i class="fa-solid fa-chevron-down" data-fa-transform="down-6"></i>
                        </button>
                        <span id="KvmDesktopButtons">
                            <input type="button" class="btn btn-success me-1 btn-sm" onclick="connectAllKvmFunction()" value="Conectar Todos">
                            <input type="button" class="btn btn-danger me-1 btn-sm" onclick="disconnectAllKvmFunction()" value="Desconectar Todos">
                            <div class="form-check form-check-inline me-1 align-middle" title="Conexión automática">
                                <input class="form-check-input me-1" type="checkbox" id="autoConnectDesktopCheckbox" onclick="autoConnectDesktops(event)">
                                <label class="form-check-label" for="autoConnectDesktopCheckbox">Automático</label>
                            </div>
                        </span>
                        <input type="button" class="btn btn-secondary me-1 btn-sm" onclick="onDevicesScroll(true);" value="Scroll To Top">
                        <input type="button" class="btn btn-primary me-1 btn-sm" onclick="showMultiDesktopSettings()" value="Opciones">
                        <input type="text" id="KvmSearchInput" class="form-control-sm me-1" placeholder="Filtrar" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Filtrar dispositivos por nombre o prefijo: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Prefijo ! para negar, p.ej. !tag:abc">
                        <span id="KvmSearchInputClearButton" style="display:none;position:relative">
                            <span class="fa-layers fa-fw" role="button" onclick="clearDeviceSearch()" style="position:absolute;left:-25px;top:-15px">
                                <i class="fa-solid fa-circle" style="color:var(--bs-secondary-bg)"></i>
                                <i class="fa-solid fa-xmark" style="color:var(--bs-body-color)"></i>
                            </span>
                        </span>
                    </div>
                    <div id="devMapToolbar" class="d-flex align-items-center style14 visually-hidden">
                        <input class="form-control-sm me-1" type="search" id="mapSearchLocation" placeholder="Ubicación de Búsqueda" onfocus="onMapSearchFocus(1)" onblur="onMapSearchFocus(0)">
                        <input class="btn btn-primary me-1 btn-sm" type="button" value="Buscar" title="Buscar ubicación" onclick="getSearchLocation()">
                        <input class="btn btn-secondary me-1 btn-sm" type="button" id="refreshmap" title="Restablecer vista del mapa" value="Reiniciar" onclick="refreshMap(false,true)">
                    </div>
                    <div class="d-flex align-items-center ms-auto">
                        <div style="display:none" id="devListToolbarView">
                            Ver
                            <select id="viewselect" onchange="onDeviceViewChange()">
                                <option value="1">Columnas</option>
                                <option value="2">Lista</option>
                                <option value="3">Anchura Fija de Escritorios</option>
                                <option id="viewselectmapoption" value="4" style="display:none">Mapa</option>
                                <option value="5">Escritorios</option>
                            </select>
                        </div>
                        <div style="display:none" id="devListToolbarSize">
                            Tamaño
                            <select id="sizeselect" class="form-select-sm me-2" onchange="onDeviceViewChange()">
                                <option value="0">Pequeño</option>
                                <option value="1">Medio</option>
                                <option value="2">Grande</option>
                            </select>
                            &nbsp;
                        </div>
                        <div style="display:none" id="devListToolbarSort">
                            Ordenar por
                            <select id="sortselect" class="form-select-sm me-2" onchange="mainUpdate(6)">
                                <option>Grupo</option>
                                <option>Encendido</option>
                                <option>Dispositivo</option>
                                <option>Etiquetas</option>
                                <option>Etiquetas de Grupo</option>
                                <option>Ultima vez visto</option>
                                <option>Last Boot Up Time</option>
                            </select>
                            &nbsp;
                        </div>
                        <i class="fa-solid fa-gear" role="button" id="devListToolbarSettings" onclick="onDeviceViewSettings()"></i>
                    </div>
                </div>
                <div id="NoMeshesPanel" style="display:none">
                    <table>
                        <tbody><tr>
                            <td valign="top" style="width:50px">
                                <img src="images/info.png" width="47" height="48">
                            </td>
                            <td>
                                <div id="getStarted1">Para empezar, <a href="#" onclick="return account_createMesh()"><strong>haz clic aquí para crear un grupo de dispositivos</strong></a>.</div>
                                <div id="getStarted2">No hay grupos de dispositivos.</div>
                            </td>
                        </tr>
                    </tbody></table>
                </div>
                <div id="xdevices" class="noselect" style="display:none" onscroll="onDevicesScroll(false)"></div>
                <div id="xdevicesmap" style="display:none">
                    <div id="xmapSearchResultsDlg" style="display:none">
                        <div id="xmapSearchResultsBck">
                            <div id="xmapSearchClose" onclick="mapCloseSearchWindow()"><b>X</b></div>
                            <div style="padding:5px">Resultados de Ubicación</div>
                            <div style="width:100%;margin:6px"></div>
                        </div>
                        <div id="xmapSearchResults" style="margin:6px"></div>
                    </div>
                </div>
                <div id="xmap-info-window"></div>
            </div>
            <div id="p2" style="display:none">
                <div id="p2title">
                    <h1>Mi Cuenta</h1>
                </div>
                <div id="p2info" style="overflow-y:auto">
                    <div id="p2AccountImageFrame">
                        <img id="p2AccountImage" alt="" width="128" height="128" onclick="account_manageImage(0)" src="images/user-256.png">
                        <div><a href="#" onclick="account_manageImage(0)">Cambiar imágen</a></div>
                    </div>
                    <div id="p2AccountSecurity" style="display:none">
                        <p><strong>Seguridad de la cuenta</strong></p>
                        <div style="margin-left:25px">
                            <div id="managePhoneNumber1">
                                <div class="p2AccountActions"><span id="authPhoneNumberCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_managePhone()">Administrar número de teléfono</a><br></span>
                            </div>
                            <div id="manageEmail2FA">
                                <div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthEmail()">Administrar autenticación por correo electrónico</a><br></span>
                            </div>
                            <div id="manageAuthApp">
                                <div class="p2AccountActions"><span id="authAppSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthApp()">Administrar la aplicación de autenticación</a><br></span>
                            </div>
                             <div id="manageDuoApp">
                                <div class="p2AccountActions"><span id="authDuoSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthDuo()">Administrar autenticación Duo</a><br></span>
                            </div>
                            <div id="manageHardwareOtp">
                                <div class="p2AccountActions"><span id="authKeySetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageHardwareOtp(0)">Administrar claves de seguridad</a><br></span>
                            </div>
                            <div id="managePushAuthDev">
                                <div class="p2AccountActions"><span id="authPushAuthDevCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_managePushAuthDev()">Gestionar la autenticación push</a><br></span>
                            </div>
                            <div id="manageMessaging1">
                                <div class="p2AccountActions"><span id="authMessagingCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageMessaging()">Administrar mensajes</a><br></span>
                            </div>
                            <div id="manageOtp">
                                <div class="p2AccountActions"><span id="authCodesSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageOtp(0)">Administrar códigos de respaldo</a><br></span>
                            </div>
                            <div class="p2AccountActions"></div><span><a href="#" onclick="return account_viewPreviousLogins()">Ver inicios de sesión anteriores</a><br></span>
                        </div>
                    </div>
                    <div id="p2AccountActions">
                        <p><strong>Acciones de la cuenta</strong></p>
                        <p class="mL">
                            <span id="managePhoneNumber2" style="display:none"><a href="#" onclick="return account_managePhone()">Administrar número de teléfono</a><br></span>
                            <span id="manageMessaging2" style="display:none"><a href="#" onclick="return account_manageMessaging()">Administrar mensajes</a><br></span>
                            <span id="verifyEmailId" style="display:none"><a href="#" onclick="return account_showVerifyEmail()">Verificar correo electrónico</a><br></span>
                            <span id="accountEnableNotificationsSpan" style="display:none"><a href="#" onclick="return account_enableNotifications()">Habilitar notificaciones web</a><br></span>
                            <a href="#" onclick="return account_showLocalizationSettings()">Opciones de Hora e Idioma</a><br>
                            <a href="#" onclick="return account_showAccountNotifySettings()">Opciones de Notificación</a><br>
                            <span id="p2AccountPassActions">
                                <span id="accountChangeEmailAddressSpan" style="display:none"><a href="#" onclick="return account_showChangeEmail()">Cambiar dirección de correo electrónico</a><br></span>
                                <a href="#" onclick="return account_showChangePassword()">Cambiar contraseña</a><span id="p2nextPasswordUpdateTime"></span><br>
                                <a href="#" onclick="return account_showDeleteAccount()">Eliminar cuenta</a><br>
                            </span>
                            <span id="accountCreateLoginTokenSpan" style="display:none"><a href="#" onclick="return account_createLoginToken()">Crear token de inicio de sesión</a><br></span>
                            <a href="#" onclick="return account_showThemesSwitcher()">Cambiar tema</a><br>
                            <span id="accountCustomIconsSpan" style="display:none"><a href="#" onclick="return showIconCustomization()">Personalización de iconos</a><br></span>
                        </p>
                    </div>
                    <div id="p2logintokens"></div>
                    <strong>Grupos de Dispositivos</strong>
                    <span id="p2createMeshLink1"> - <button class="btn btn-primary btn-sm" onclick="return account_createMesh()"><i class="fa-fw fa-solid fa-circle-plus"></i> Nuevo</button></span>
                    <br><br>
                    <div id="p2meshes"></div>
                    <div id="p2noMeshFound" style="display:none">No hay grupos de dispositivos.<span id="p2createMeshLink2"> <a href="#" onclick="return account_createMesh()"><strong>¡Comienza aquí!</strong></a></span>
                    </div>
                    <br style="clear:both">
                </div>
            </div>
            <div id="p3" style="display:none">
                <div id="p3title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis Eventos</div>
                </div>
                <table class="pTable">
                    <tbody><tr>
                        <td class="auto-style1 d-flex justify-content-end p-1">
                            <div class="d-flex align-items-center">
                                <label for="p3filterevents" class="me-1">Filtrar</label>
                                <select id="p3filterevents" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="">All Logs</option>
                                    <option notransval="1" value="agentlog">Agent Logs</option>
                                    <option notransval="1" value="relaylog">Relay Logs</option>
                                    <option notransval="1" value="manual">Manual Logs</option>
                                    <option notransval="1" value="runcommands">Run Command Logs</option>
                                    <option notransval="1" value="batchupload">Batch Upload Logs</option>
                                    <option notransval="1" value="changenode">Change Node Logs</option>
                                    <option notransval="1" value="removenode">Remove Node Logs</option>
                                </select>
                                <label for="p3limitdropdown" class="me-1">Mostrar</label>
                                <select id="p3limitdropdown" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="60">Últimos 60</option>
                                    <option notransval="1" value="120">Últimos 120</option>
                                    <option notransval="1" value="250">Últimos 250</option>
                                    <option notransval="1" value="500">Últimos 500</option>
                                    <option notransval="1" value="1000">Últimos 1000</option>
                                    <option notransval="1" value="">Sin límite</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(2)"><i class="fa-solid fa-download" title="Descargar Eventos"></i></a>
                            </div>
                        </td>
                    </tr>
                </tbody></table>
                <div id="p3events"></div>
            </div>
            <div id="p4" style="display:none">
                <div id="p4title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis Usuarios</div>
                </div>
                <table class="pTable">
                    <tbody><tr>
                        <td id="p4toolbarActions" class="style14 d-flex align-items-center flex-wrap p-1">
                            <div class="d-flex align-items-center">
                                <input type="button" id="UsersSelectAllButton" class="btn btn-secondary btn-sm me-1" onclick="p3usersSelectallButtonFunction()" value="Seleccionar Todo">
                                <input type="button" id="UsersGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Acción de Grupo" onclick="p3usersGroupActionFunction()">
                                <input id="UserNewAccountButton" class="btn btn-success btn-sm me-1" type="button" onclick="showCreateNewAccountDialog()" value="Nueva Cuenta...">
                                <div class="btn-group me-1 mobileActionDropdown">
                                    <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">Más&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
                                    <div class="dropdown-menu">
                                        <button type="button" class="dropdown-item" onclick="showUserBroadcastDialog()">Broadcast</button>
                                        <button type="button" class="dropdown-item" onclick="p4downloadUserInfo()">Descargar información del usuario</button>
                                        <button type="button" id="p4MobileUserBatchCreate" class="dropdown-item" style="display:none" onclick="p4batchAccountCreate()">Crear cuentas por lotes</button>
                                        <button type="button" class="dropdown-item" onclick="onUsersViewSettings()">Opciones</button>
                                    </div>
                                </div>
                                <div class="d-inline-flex align-items-center">
                                    <input id="UserSearchInput" type="search" class="form-control-sm me-1" placeholder="Filtrar" onchange="onUserSearchInputChanged()" onkeyup="onUserSearchInputChanged()" autocomplete="off" onfocus="onUserSearchFocus(1)" onblur="onUserSearchFocus(0)">
                                </div>
                            </div>
                            <div class="d-flex align-items-center ms-auto desktopAction">
                                <input type="button" class="btn btn-primary btn-sm me-2" onclick="showUserBroadcastDialog()" style="margin-right:6px" value="Broadcast">
                                <a href="#" onclick="p4downloadUserInfo()"><i role="button" class="fa-solid fa-download me-2" title="Descargar información del usuario"></i></a>
                                <a href="#" onclick="p4batchAccountCreate()"><i role="button" id="p4UserBatchCreate" style="display:none" title="Creación por lote de cuentas de usuario" class="fa-solid fa-upload me-1"></i></a>
                                <i style="cursor:pointer;margin-top:3px;margin-left:6px" id="usersListToolbarSettings" class="fa-solid fa-cog" title="Opciones" onclick="onUsersViewSettings()"></i>
                            </div>
                        </td>
                    </tr>
                </tbody></table>
                <div id="p3users"></div>
            </div>
            <div id="p5" style="display:none">
                <div id="p5title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis Archivos</div>
                </div>
                <table id="p5toolbar" class="table" cellpadding="0" cellspacing="0">
                    <tbody><tr>
                        <td id="p5filehead" valign="bottom" class="d-flex align-items-center">
                            <div id="p5rightOfButtons" class="d-flex align-items-center"></div>
                            <div class="m-1 d-flex align-items-center">
                                <input type="button" id="p5FolderUp" class="btn btn-secondary btn-sm me-1" disabled="disabled" onclick="return p5folderup();" value="Subir">
                                <input type="button" id="p5SelectAllButton" class="btn btn-secondary btn-sm me-1" disabled="disabled" onclick="p5selectallfile();" value="Seleccionar Todo">
                                <input type="button" id="p5RenameFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Renombrar" onclick="p5renamefile();">
                                <input type="button" id="p5DeleteFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Eliminar" onclick="p5deletefile();">
                                <input type="button" id="p5ViewFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Editar" onclick="p5viewfile()">
                                <input type="button" id="p5NewFolderButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Nueva Carpeta" onclick="p5createfolder();">
                                <input type="button" id="p5UploadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Subir Archivo" onclick="p5uploadFile()">
                                <input type="button" id="p5DownloadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Descargar" onclick="p5downloadButton()">
                                <input type="button" id="p5CutButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Cortar" onclick="p5copyFile(1)">
                                <input type="button" id="p5CopyButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Copiar" onclick="p5copyFile(0)">
                                <input type="button" id="p5PasteButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Pegar" onclick="p5pasteFile()">
                                <div class="btn-group me-1 mobileActionDropdown">
                                    <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">Más&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
                                    <div class="dropdown-menu">
                                        <button type="button" disabled="disabled" id="p5MobileNewFolderButton" class="dropdown-item" onclick="p5createfolder()">Nueva Carpeta</button>
                                        <button type="button" disabled="disabled" id="p5MobileUploadButton" class="dropdown-item" onclick="p5uploadFile()">Subir Archivo</button>
                                        <button type="button" disabled="disabled" id="p5MobileDownloadButton" class="dropdown-item" onclick="p5downloadButton()">Descargar</button>
                                        <button type="button" disabled="disabled" id="p5MobileCutButton" class="dropdown-item" onclick="p5copyFile(1)">Cortar</button>
                                        <button type="button" disabled="disabled" id="p5MobileCopyButton" class="dropdown-item" onclick="p5copyFile(0)">Copiar</button>
                                        <button type="button" disabled="disabled" id="p5MobilePasteButton" class="dropdown-item" onclick="p5pasteFile()">Pegar</button>
                                    </div>
                                </div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td id="p5filesubhead" class="d-flex align-items-center">
                            <div class="d-flex align-items-center">&nbsp;&nbsp;<span id="p5currentpath"></span></div>
                            <div style="float:right" class="d-flex align-items-center ms-auto">
                                <select id="p5sortdropdown" class="form-select-sm me-1" onchange="updateFiles()">
                                    <option value="1" selected="selected">Orden: ascendente, por nombre</option>
                                    <option value="2">Orden: ascendente, por tamaño</option>
                                    <option value="3">Orden: ascendente, por fecha</option>
                                    <option value="4">Orden: Descendente, por nombre</option>
                                    <option value="5">Orden: Descendente, por tamaño</option>
                                    <option value="6">Orden: Descendente, por fecha</option>
                                </select>
                            </div>
                        </td>
                    </tr>
                </tbody></table>
                <div id="p5filetable">
                    <!--
                    <form id=p5fileCatchAll method=post enctype=multipart/form-data action=uploadfile.ashx target=fileUploadFrame>
                        <input type=file id=p5fileCatchAllInput name=files style="position:absolute;left:0;width:100%;top:0;bottom:0;opacity:0;display:none" onchange="p5fileCatchAllInputChanged(event)" />
                        <input id=p5fileDragLink2 name="link" style="display:none" />
                        <input type=submit id=p5fileCatchAllSubmit style="display:none" />
                    </form>
                    -->
                    <div id="p5PublicShare" style="">
                        <div>Estos archivos se comparten públicamente, haz clic en "enlace" para obtener la URL pública.</div>
                    </div>
                    <div id="bigok" style="display:none"><b>✓</b></div>
                    <div id="bigfail" style="display:none"><b>✗</b></div>
                    <span id="p5files"></span>
                </div>
                <table id="p5toolbarBottom" class="table table-hover" style="width:100%" cellpadding="0" cellspacing="0">
                    <tbody><tr>
                        <td class="style6">&nbsp;<span id="p5bottomstatus"></span></td>
                    </tr>
                </tbody></table>
            </div>
            <div id="p6" style="display:none">
                <div id="p6info" style="overflow-y:auto">
                    <div id="p6title" class="d-flex align-items-center">
                        <div class="fs-4 fw-bold">Mi Servidor</div>
                        <img id="MainMeshImage" src="serverpic.ashx" class="ms-auto">
                    </div>
                    <div id="p2ServerActions">
                        <p><strong>Acciones del Servidor</strong></p>
                        <div style="margin-left:25px">
                            <div id="p2ServerActionsBackup">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showBackupDlg()">Descargar copia de seguridad del servidor</a>
                            </div>
                            <div id="p2ServerActionsRestore">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showRestoreDlg()">Restaurar servidor con copia de seguridad</a>
                            </div>
                            <div id="p2ServerActionsGoogleBackup" style="display:none">
                                <div class="p2AccountActions"><span id="p2ServerActionsGoogleBackupCheck" style="display:none"><strong>✓</strong></span></div><span><a href="#" onclick="return server_setupGoogleDriveBackup()">Resguardo en Google Drive</a><br></span>
                            </div>
                            <div id="p2ServerActionsVersion">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showVersionDlg()">Verificar la versión del servidor</a>
                            </div>
                            <div id="p2ServerActionsErrors">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showErrorsDlg()">Mostrar registro de errores del servidor</a>
                            </div>
                            <div id="p2ServerActionsConfig">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showConfigDlg()">Show server configuration</a>
                            </div>
                        </div>
                        <br>
                    </div>
                    <strong>Estadísticas del Servidor</strong><br><br>
                    <div id="serverStats">
                        <div id="serverCpuChartView" style="display:none">
                            <div class="chartViewCanvas" style="height:40px; text-align:center"><canvas id="serverCpuChart" style="display:inline-block"></canvas></div>
                            <div class="chartViewText" id="serverCpuChartText"></div>
                        </div>
                        <div id="serverMemoryChartView" style="display:none">
                            <div class="chartViewCanvas" style="height:40px; text-align:center"><canvas id="serverMemoryChart" style="display:inline-block"></canvas></div>
                            <div class="chartViewText" id="serverMemoryChartText"></div>
                        </div><br><br>
                        <div id="serverStatsTable"></div>
                    </div>
                    <div id="serverWarningsDiv" style="display:none">
                        <br><strong>Advertencias del Servidor</strong><br><br>
                        <div id="serverCertWarnings"></div>
                        <div id="serverWarnings"></div>
                    </div>
                </div>
            </div>
            <div id="p10" style="display:none">
                <div id="p10title" class="d-flex align-items-center">
                    <div id="p10BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-circle-info fa-sm me-2 text-secondary"></i>General<span id="p10deviceName"></span></div>
                </div>
                <div id="p10info" style="overflow-y:auto" class="pt-3">
                    <table style="width:100%" cellpadding="0" cellspacing="0">
                        <tbody><tr>
                            <td style="width:auto" valign="top">
                                <div id="p10html"></div>
                            </td>
                            <td style="width:100px;display:none" id="notesPanel" valign="top">
                                <table>
                                    <thead>
                                        <tr>
                                            <th>Notas</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr>
                                            <td><div id="notesPanelArea" style="width:300px;height:200px;resize:none;overflow-y:scroll"></div></td>
                                        </tr>
                                    </tbody>
                                </table>
                            </td>
                            <td style="width:20px"></td>
                            <td style="width:200px;vertical-align:top;position:relative" valign="top">
                                <div class="deviceNotifyLargeDot">
                                    <div id="p10deviceMsg" onclick="showDeviceMessages(null,null,event)" class="deviceNotifyDotSub"></div>
                                    <i title="Destacado" role="button" class="fa-solid fa-star" id="p10deviceStar" onclick="showDeviceSessions(null,null,event)" data-fa-transform="shrink-5" data-fa-mask="fa-solid fa-circle"></i>
                                    <i title="Lista de sesiones remotas activas en esta computadora." role="button" class="fa-solid fa-arrow-right-arrow-left" id="p10deviceNotify" onclick="showDeviceSessions(null,null,event)" data-fa-transform="shrink-5" data-fa-mask="fa-solid fa-circle"></i>
                                    <i title="Ayuda Solicitada" role="button" class="fa-solid fa-question" id="p10deviceHelp" onclick="showDeviceHelpRequests(null,null,event)" data-fa-transform="shrink-5" data-fa-mask="fa-solid fa-circle"></i>
                                </div>
                                <div id="p10deviceBattery" class="deviceBatteryLarge deviceBatteryLarge1"></div>
                                <a href="#" onclick="p10showiconselector()"><img id="MainComputerImage"></a>
                                <div id="MainComputerState"></div>
                            </td>
                        </tr>
                    </tbody></table><br>
                    <div id="p10html2"></div>
                    <div id="p10html3"></div>
                    <div id="p10html4"></div>
                    <div id="p10html5"></div>
                </div>
            </div>
            <div id="p11" class="noselect" style="display:none">
                <!-- softKeyboard: lives at p11 level (outside deskarea4) so it stays
                     focusable even when the toolbar is hidden in fullscreen/landscape.
                     position:fixed keeps it off-screen while remaining a valid focus target. -->
                <input id="softKeyboard" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" type="text" inputmode="text" style="position:fixed;opacity:0;width:1px;height:1px;left:-9999px;top:-9999px" oninput="onSoftKeyboardInput(this)" onblur="onSoftKeyboardFocusChange(false)" onfocus="onSoftKeyboardFocusChange(true)">
                <!-- Trackpad cursor -->
                <svg id="trackpadCursor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 20" width="18" height="30" style="display:none;position:fixed;pointer-events:none;z-index:9999;transform-origin:0 0;filter:drop-shadow(1px 1px 2px rgba(0,0,0,0.8))">
                    <path d="M0,0 L0,16 L4,12 L6.5,18 L8.5,17 L6,11 L11,11 Z" fill="white" stroke="black" stroke-width="1.2" stroke-linejoin="round"></path>
                </svg>
                <div id="p11title" class="d-flex align-items-center">
                    <div id="p11BackButton" class="pe-2">
                        <i class="fa-solid fa-2xl fa-square-caret-left" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-display fa-sm me-2 text-secondary"></i>Escritorio<span id="p11deviceName"></span></div>
                    <div id="p11KeyboardLights" style="font-size:x-small;color:black" class="ms-auto pe-2">
                        <div id="p11numlock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">NÚMERO</div>
                        <div id="p11capslock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">TAPAS</div>
                        <div id="p11scrolllock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">DESPLAZARSE</div>
                    </div>
                    <div id="devListToolbarViewIcons">
                        <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick="deskToggleFull(event)" title="Pantalla Completa. Mantenga presionada la tecla Mayús para que el navegador ocupe toda la pantalla."></i>
                    </div>
                </div>
                <div id="p11warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">El puerto de redirección o la función KVM están deshabilitados<span id="p11warninga">, haga click aquí para habilitarlo.</span></div>
                </div>
                <div id="p11warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">La computadora remota no está encendida, haz clic aquí para enviar el comando de encendido.
                    </div>
                </div>
                <div id="deskarea0" cellpadding="0" cellspacing="0">
                    <div id="deskarea1" class="areaHead d-flex flex-wrap">
                        <div class="d-flex align-items-center">
                            <input type="button" class="btn btn-primary btn-sm me-1" id="autoconnectbutton1" value="AutoConectar" onclick="autoConnectDesktop(event)" onkeypress="return false" onkeydown="return false" style="display:none;margin-right:4px">
                            <div class="btn-group dropdown-center me-1" id="connectbutton1span">
                                <button type="button" class="btn btn-success btn-sm" id="connectbutton1" title="Conéctese usando el escritorio remoto de MeshAgent" onclick="connectDesktop(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Conectar</button>
                                <button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" id="connectbutton1d" data-bs-toggle="dropdown" aria-expanded="false">
                                    <span class="visually-hidden">Alternar desplegable</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(1,event)">Pedir Consentimiento + Barra</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(2,event)">Pedir Consentimiento</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(3,event)">Barra de Privacidad</a></li>
                                </ul>
                            </div>
                            <div class="btn-group dropdown-center me-1" id="connectbutton1rspan">
                                <button type="button" class="btn btn-success btn-sm" id="connectbutton1r" title="Conéctate usando RDP" onclick="askRdpCredentials()" onkeypress="return false" onkeydown="return false" disabled="disabled">Conexión RDP</button>
                                <button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" id="connectbutton1rd" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">Alternar desplegable</span></button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmaltportaction(1,event)">Alternar Puerto</a></li>
                                </ul>
                            </div>
                            <span id="connectbutton1hspan"><button class="btn btn-primary btn-sm me-1" type="button" id="connectbutton1h" title="Conéctese usando hardware KVM" onclick="connectDesktop(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">Conectar al HW</button></span>
                            <div class="btn-group dropdown-center me-1" id="disconnectbutton1span">
                                <button type="button" class="btn btn-danger btn-sm" id="disconnectbutton1" title="Conéctese usando el escritorio remoto de MeshAgent" onclick="connectDesktop(event,0)" onkeypress="return false" onkeydown="return false">Desconectar</button>
                                <button type="button" class="btn btn-danger btn-sm dropdown-toggle dropdown-toggle-split" id="diconnectbutton1d" data-bs-toggle="dropdown" aria-expanded="false">
                                    <span class="visually-hidden">Alternar desplegable</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(10,event)">Desconectar y bloquear</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(11,event)">Desconectar</a></li>
                                </ul>
                            </div>
                            <span id="deskstatus" style="line-height:22px">Desconectado</span><span id="deskmetadata"></span>
                        </div>
                        <div class="d-flex align-items-center ms-auto">
                            <div id="desktopCustomUiButtons" style="float:left"></div>
                            <div id="desktopCustomUpperRight" style="float:left;margin-right:6px"></div>
                            <input type="button" class="btn btn-primary btn-sm" title="Cambiar el estado de energía de la máquina remota" onkeypress="return false" onkeydown="return false" value="Acciones de Encendido..." onclick="showPowerActionDlg()" style="display:none">
                            <input id="deskActionsBtn" type="button" class="btn btn-primary btn-sm me-1" title="Realizar acciones de energía en el dispositivo" onkeypress="return false" onkeydown="return false" value="Acciones" onclick="deviceActionFunction()">
                            <input id="deskActionsSettings" type="button" class="btn btn-primary btn-sm me-1" value="Opciones" title="Editar configuraciones de escritorio remoto" onkeypress="return false" onkeydown="return false" onclick="showDesktopSettings()">
                            <input id="deskFocusBtn" type="button" title="Alternar el modo de enfoque, cuando está activo solo se actualiza la región alrededor del mouse" onkeypress="return false" onkeydown="return false" value="Enfocar Todo" onclick="deskToggleFocus()" style="margin-right:3px;display:none">
                            <div id="deskRecordIcon" class="deskareaicon" title="El servidor está grabando esta sesión" style="display:none;background-color:red;width:12px;height:12px;border-radius:6px;margin-top:5px"></div>
                            <div class="deskareaicon" title="Gira a la Derecha" onclick="drotate(1)"><i class="fa-solid fa-rotate-right"></i></div>
                            <div class="deskareaicon" title="Girar a la Izquierda" onclick="drotate(-1)"><i class="fa-solid fa-rotate-left"></i></div>
                            <div class="deskareaicon" title="Alternar Modo de Vista" onclick="toggleAspectRatio(1)">⇲</div>
                            <div id="idx_deskFullBtn2" onclick="deskToggleFull(event)" style="float:right">&nbsp;✖</div>
                        </div>
                    </div>
                    <div id="deskarea3x">
                        <div id="p11bigok" style="display:none"><b>✓</b></div>
                        <div id="p11bigfail" style="display:none"><b>✗</b></div>
                        <div id="DeskFocus" oncontextmenu="return false" onmousedown="dmousedown(event)" onmouseup="dmouseup(event)" onmousemove="dmousemove(event)"></div>
                        <div id="DeskParent">
                            <canvas id="Desk" width="640" height="480" oncontextmenu="return false" onmousedown="dmousedown(event)" onmouseup="dmouseup(event)" onmousemove="dmousemove(event)" onmousewheel="dmousewheel(event)" ontouchstart="dtouchstart(event)" ontouchmove="dtouchmove(event)" ontouchend="dtouchend(event)"></canvas>
                        </div>
                        <div id="DeskTools">
                            <div id="deskToolsAreaTop">
                                <a id="DeskToolsRefreshButton" class="link-underline-primary" style="right:2px" onclick="refreshDeskTools()">Actualizar</a>
                                <div id="deskToolsTopTabProcess" class="deskToolsTopTab" onclick="changeDeskToolTab(0)" style="left:0px;bottom:0px">Procesos</div>
                                <div id="deskToolsTopTabService" class="deskToolsTopTab" onclick="changeDeskToolTab(1)" style="display:none;left:90px;color:gray">Servicios</div>
                            </div>
                            <div id="deskToolsArea">
                                <div id="DeskToolsProcessTab">
                                    <div id="deskToolsHeader">
                                        <a class="colmn1" title="Ordenar por id de proceso" onclick="sortProcess(0)">PID</a>
                                        <a class="colmn2" title="Orden: ascendente, por nombre" onclick="sortProcess(1)">Nombre</a>
                                    </div>
                                    <div id="DeskToolsProcesses" style=""></div>
                                </div>
                                <div id="DeskToolsServiceTab" style="display:none">
                                    <div id="deskToolsServiceHeader">
                                        <a class="colmn1" style="width:70px" title="Ordenar por estado" onclick="sortService(0)">Estado</a>
                                        <a class="colmn2" title="Orden: ascendente, por nombre" onclick="sortService(1)">Nombre</a>
                                    </div>
                                    <div id="DeskToolsServices" style=""></div>
                                </div>
                            </div>
                        </div>
                        <div id="p11DeskConsoleMsg" style="display:none;text-align:left;cursor:pointer;position:absolute;left:30px;top:17px;color:yellow;background-color:rgba(0,0,0,0.6);padding:10px;border-radius:5px;text-align:left" onclick="p11clearConsoleMsg()"></div>
                        <div id="p11DeskSessionSelector" style="display:none;position:absolute;left:30px;top:17px;right:30px;bottom:17px;overflow-y:auto">
                        </div>
                    </div>
                    <div id="deskarea4" class="areaFoot d-flex flex-wrap">
                        <div class="d-flex align-items-center">
                            <select id="deskkeys" class="form-select-sm me-1" cmenu="deskKeyShortcutContextMenu"></select>
                            <button id="DeskWD" class="btn btn-primary btn-sm me-1" onkeypress="return false" onkeydown="return false" onclick="deskSendKeys()">Enviar</button>
                            <input id="DeskESC" style="display:none" type="button" class="btn btn-secondary btn-sm me-1" value="ESC" onkeypress="return false" onkeydown="return false" onclick="sendDeskEsc()">
                            <input id="DeskClip" type="button" class="btn btn-secondary btn-sm me-1" value="Portapapeles" onkeypress="return false" onkeydown="return false" onclick="showDeskClip()">
                            <input id="DeskType" class="btn btn-secondary btn-sm me-1" cmenu="deskPreConfigShortcutContextMenu" type="button" value="Tipo" onkeypress="return false" onkeydown="return false" onclick="showDeskType()">
                            <!-- Mobile-only: toggle button for the soft keyboard -->
                            <button id="DeskSoftKbdBtn" class="btn btn-secondary btn-sm me-1" type="button" onclick="toggleMobileKeyboard()" title="Mostrar/ocultar teclado en pantalla" style="display:none">
                                <i class="fa-solid fa-keyboard"></i>
                            </button>
                            <div id="DeskControlSpan" title="Alternar control de mouse y teclado de forma remota" class="form-check"><input id="DeskControl" type="checkbox" class="form-check-input" onkeypress="return false" onkeydown="return false" onclick="toggleKvmControl()"><label class="form-check-label" for="DeskControl">Controlar mouse y teclado de forma remota</label></div>&nbsp;
                        </div>
                        <!-- Mobile-only "More" button — opens a fixed-position action panel -->
                        <div id="deskMobileActions" class="ms-auto align-items-center" style="display:none">
                            <button class="btn btn-secondary btn-sm" type="button" onclick="deskToggleMobileActions()" title="Más acciones">
                                Más&nbsp;<i class="fa-solid fa-chevron-up fa-xs"></i>
                            </button>
                        </div>
                        <div id="deskarea4Icons" class="d-flex ms-auto align-items-center">
                            <span id="DeskMonitorSelectionSpan"></span>
                            <span id="DeskLatency" style="width:70px" class="text-center" title="Latencia de Sesión de Escritorio"></span>
                            <span id="DeskTimer" style="width:70px" class="text-center" title="Tiempo de sesión"></span>
                            <input id="DeskToolsButton" type="button" class="btn btn-primary btn-sm mx-1" value="Herramientas" title="Alternar vista de herramientas" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()">
                            <span id="DeskGuestShareButton" title="Compartir el dispositivo con un invitado" role="button">
                                <i class="fa-solid fa-fw fa-share-nodes" id="DeskInputUnLockedButtonImage" onclick="showShareDevice()"></i>
                            </span>
                            <span id="DeskInputUnLockedButton" title="La entrada remota está desbloqueada" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" onclick="deskInputLockFunction(1)"></i>
                            </span>
                            <span id="DeskInputLockedButton" title="La entrada remota está bloqueada" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" style="color:red" onclick="deskInputLockFunction(0)"></i>
                            </span>
                            <span id="DeskRefreshButton" title="Actualizar el escritorio" role="button">
                                <i class="fa-solid fa-fw fa-rotate" onclick="deskRefreshFunction()"></i>
                            </span>
                            <span id="DeskClipboardOutButton" title="Cargar Portapapeles local a dispositivo remoto" role="button">
                                <i class="fa-solid fa-fw fa-up-long" data-fa-transform="shrink-9 down-4" data-fa-mask="fa-solid fa-clipboard" onclick="deskClipboardOutFunction()"></i>
                            </span>
                            <span id="DeskClipboardInButton" title="Descargar portapapeles remoto a portapapeles local" style="display:none" role="button">
                                <i class="fa-solid fa-fw fa-down-long" data-fa-transform="shrink-9 down-4" data-fa-mask="fa-solid fa-clipboard" onclick="deskClipboardInFunction()"></i>
                            </span>
                            <span id="DeskRecordButton" cmenu="deskPlayerContextMenu" title="Grabar sesión de escritorio remoto en archivo" style="display:none" role="button">
                                <i class="fa-solid fa-fw fa-video" onclick="deskRecordSession()" id="DeskRecordButtonImage"></i>
                            </span>
                            <span id="DeskSaveImageButton" title="Guardar una captura de pantalla del escritorio remoto" role="button">
                                <i class="fa-solid fa-fw fa-camera" onclick="deskSaveImage()"></i>
                            </span>
                            <span id="DeskBackgroundButton" title="Alternar fondo de escritorio remoto" role="button">
                                <i class="fa-solid fa-fw fa-image" onclick="deviceToggleBackground(event)" id="DeskBackgroundButtonImage"></i>
                            </span>
                            <span id="DeskOpenWebButton" title="Abre una dirección web en la computadora remota" role="button">
                                <i class="fa-solid fa-fw fa-globe" onclick="deviceUrlFunction()"></i>
                            </span>
                            <span id="DeskLockButton" title="Bloquear la computadora remota" role="button">
                                <i class="fa-solid fa-fw fa-lock" onclick="deviceLockFunction()"></i>
                            </span>
                            <span id="DeskNotifyButton" title="Mostrar una notificación en la computadora remota" role="button">
                                <i class="fa-solid fa-fw fa-bell" onclick="deviceToastFunction()"></i>
                            </span>
                            <span id="DeskChatButton" class="deskarea" title="Abrir ventana de chat en esta computadora" role="button">
                                <i class="fa-solid fa-fw fa-message" onclick="deviceChat(event)"></i>
                            </span>
                            <span id="DeskRunButton" cmenu="deskPreConfigScriptContextMenu" class="deskarea" title="Ejecutar un Script en esta computadora" role="button">
                                <i class="fa-solid fa-fw fa-play" onclick="runDeviceCmd()"></i>
                            </span>
                        </div><!-- /deskarea4Icons -->
                    </div>
                    <!-- Mobile action panel: fixed above the tab bar, shown via deskToggleMobileActions() -->
                    <div id="deskMobileActionsBackdrop" onclick="deskCloseMobileActions()" style="display:none;position:fixed;inset:0;z-index:1998"></div>
                    <div id="deskMobileActionsPanel" style="display:none;position:fixed;right:8px;bottom:82px;z-index:1999;min-width:220px;border-radius:12px;overflow-y:auto;overflow-x:hidden;max-height:calc(100dvh - 100px);box-shadow:0 4px 24px rgba(0,0,0,0.28)">
                        <div id="deskModifierBar" class="d-flex gap-2 px-3 py-2 border-bottom" style="background:var(--bs-body-bg,#fff)">
                            <button id="deskModCtrl" type="button" class="desk-mod-key" onclick="toggleMobileModifier('ctrl')">CTRL</button>
                            <button id="deskModShift" type="button" class="desk-mod-key" onclick="toggleMobileModifier('shift')">SHIFT</button>
                            <button id="deskModAlt" type="button" class="desk-mod-key" onclick="toggleMobileModifier('alt')">ALT</button>
                        </div>
                        <div id="deskMobileMonitorRow" style="display:none;background:var(--bs-body-bg,#fff)">
                            <div class="d-flex align-items-center px-3 py-2 border-bottom" style="font-size:15px;color:var(--bs-body-color,#212529)"><i class="fa-solid fa-desktop fa-fw me-2 text-primary"></i>Seleccionar monitor<span id="deskMobileMonitorIcons" class="ms-auto d-flex gap-2" style="font-size:20px"></span></div>
                        </div>
                        <a id="deskKbdItem" class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="toggleMobileKeyboard();deskCloseMobileActions()"><i class="fa-solid fa-keyboard fa-fw me-2 text-primary"></i>Teclado en pantalla</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDeskType();deskCloseMobileActions()"><i class="fa-solid fa-font fa-fw me-2 text-primary"></i>Escribir texto</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceActionFunction();deskCloseMobileActions()"><i class="fa-solid fa-bolt fa-fw me-2 text-primary"></i>Acciones</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDesktopSettings();deskCloseMobileActions()"><i class="fa-solid fa-gear fa-fw me-2 text-primary"></i>Opciones</a>
                        <a id="deskTrackpadItem" class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskToggleTrackpad();deskCloseMobileActions()"><i class="fa-solid fa-computer-mouse fa-fw me-2 text-primary"></i>Habilitar modo touchpad</a>
                        <a id="deskRightClickItem" class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskToggleRightClickMode();deskCloseMobileActions()"><i class="fa-solid fa-hand-pointer fa-fw me-2 text-primary"></i>Habilitar modo clic derecho</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskCustomizeKeys();deskCloseMobileActions()"><i class="fa-solid fa-sliders fa-fw me-2 text-primary"></i>Personalizar atajos</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskRefreshFunction();deskCloseMobileActions()"><i class="fa-solid fa-rotate fa-fw me-2 text-primary"></i>Actualizar escritorio</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDeskClip();deskCloseMobileActions()"><i class="fa-solid fa-paste fa-fw me-2 text-primary"></i>Portapapeles</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskClipboardOutFunction();deskCloseMobileActions()"><i class="fa-solid fa-clipboard fa-fw me-2 text-primary"></i>Subir portapapeles</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceUrlFunction();deskCloseMobileActions()"><i class="fa-solid fa-globe fa-fw me-2 text-primary"></i>Abrir URL en el equipo remoto</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceLockFunction();deskCloseMobileActions()"><i class="fa-solid fa-lock fa-fw me-2 text-primary"></i>Bloquear remoto</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceToastFunction();deskCloseMobileActions()"><i class="fa-solid fa-bell fa-fw me-2 text-primary"></i>Enviar notificación</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceChat(event);deskCloseMobileActions()"><i class="fa-solid fa-message fa-fw me-2 text-primary"></i>Chat</a>
                        <a class="d-block px-3 py-2 text-decoration-none" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="runDeviceCmd();deskCloseMobileActions()"><i class="fa-solid fa-play fa-fw me-2 text-primary"></i>Ejecutar script</a>
                    </div>
                    <!-- Fullscreen floating button - replaces all toolbars when in fulldesk mode -->
                    <div id="deskFsBtn" onclick="deskToggleMobileActions()" style="display:none;position:fixed;bottom:54px;right:12px;z-index:2000;width:48px;height:48px;border-radius:50%;background:rgba(0,0,0,0.55);color:#fff;align-items:center;justify-content:center;cursor:pointer;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);border:1.5px solid rgba(255,255,255,0.22);font-size:20px">
                        <i class="fa-solid fa-bars"></i>
                    </div>
                </div>
            </div>
            <div id="p12" style="display:none">
                <div id="p12title" class="d-flex align-items-center">
                    <div id="p12BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-terminal fa-sm me-2 text-secondary"></i>Terminal<span id="p12deviceName"></span></div>
                    <div id="devListToolbarViewIcons2" class="ms-auto">
                        <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick="deskToggleFull(event)" title="Pantalla Completa. Mantenga presionada la tecla Mayús para que el navegador ocupe toda la pantalla."></i>
                    </div>
                </div>
                <div id="p12warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">El puerto de redirección o la función KVM están deshabilitados<span id="p12warninga">, haga click aquí para habilitarlo.</span></div>
                </div>
                <div id="p12warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">La computadora remota no está encendida, haz clic aquí para enviar el comando de encendido.
                </div>
                </div>
                <div id="termTable" style="position:relative">
                    <table style="width:100%" cellpadding="0" cellspacing="0">
                        <tbody><tr>
                            <td class="areaHead d-flex flex-wrap">
                                <div class="d-flex align-items-center">
                                    <button type="button" id="autoconnectbutton2" class="btn btn-primary btn-sm me-1" onclick="autoConnectTerminal(event)" onkeypress="return false" onkeydown="return false" style="display:none"><i class="fa-solid fa-play"></i>
                                        AutoConectar</button>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2span">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2" title="Conéctese usando el escritorio remoto de MeshAgent" onclick="connectTerminal(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Conectar</button>
                                        <button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" id="connectbutton2d" data-bs-toggle="dropdown" aria-expanded="false" onclick="newContextMenu(event,1)"><span class="visually-hidden">Alternar desplegable</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Administrar Shell</b></a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0,event)">Administrar Powershell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Shell del Usuario</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Usuario PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0x10,event)">Pregunte a la consola de administración</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0x10,event)">Pregunte al administrador de PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0x10,event)">Preguntar usuario Shell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0x10,event)">Preguntar al usuario PowerShell</a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(8,0,event)"><b>Shell del Usuario</b></a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Usuario PowerShell</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Shell Root</b></a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Shell del Usuario</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(100,0,event)">Shell de inicio de sesión</a></li>
                                        </ul>
                                    </div>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2sspan">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2s" title="Conéctese usando el escritorio remoto de MeshAgent" onclick="connectTerminal(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Conexión SSH</button>
                                        <button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" id="connectbutton2sd" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">Alternar desplegable</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Alternar Puerto</a></li>
                                        </ul>
                                    </div>
                                    <span id="connectbutton2hspan"><button type="button" class="btn btn-success btn-sm me-1" id="connectbutton2h" title="Conéctate mediante hardware KVM Intel® AMT" onclick="connectTerminal(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">Conectar al HW</button></span>
                                    <span id="disconnectbutton2span"><button type="button" class="btn btn-danger btn-sm me-1" id="disconnectbutton2" onclick="connectTerminal(event,0)" onkeypress="return false" onkeydown="return false">Desconectar</button></span>
                                    <span id="termstatus" style="line-height:22px">Desconectado</span><span id="termtitle"></span>
                                </div>
                                <div class="d-flex align-items-center ms-auto">
                                    <div id="idx_termFullBtn2" onclick="deskToggleFull(event)" style="float:right">
                                        &nbsp;✖</div>
                                    <div id="termRecordIcon" class="deskareaicon" title="El servidor está grabando esta sesión" style="display:none;background-color:red;width:12px;height:12px;border-radius:6px;margin-top:5px;margin-left:5px">
                                    </div>
                                    <input id="termActionsBtn" type="button" title="Realizar acciones de energía en el dispositivo" onkeypress="return false" onkeydown="return false" value="Acciones" class="btn btn-primary me-1 btn-sm" onclick="deviceActionFunction()">
                                    <div id="terminalCustomUpperRight" style="float:left;margin-right:6px"></div>
                                    <div id="terminalCustomUiButtons" style="float:left"></div>
                                </div>
                            </td>
                        </tr>
                        <tr>
                            <td id="termarea3x">
                                <div id="termarea3xdiv" style="width:100%;height:100%;text-align:left"></div>
                                <pre id="Term"></pre>
                            </td>
                        </tr>
                        <tr>
                            <td class="areaFoot d-flex flex-wrap">
                                <div class="d-flex align-items-center">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary btn-sm me-1" id="ctrlcbutton" value="Ctl-C" onclick="termSendKey(3,'ctrlcbutton')">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary btn-sm me-1" id="ctrlxbutton" value="Ctl-X" onclick="termSendKey(24,'ctrlxbutton')">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" id="escbutton" value="ESC" onclick="termSendKey(27,'escbutton')">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" id="tabbutton" value="Tab" onclick="termSendKey(9,'tabbutton')" style="display:none">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" id="bsbutton" value="Retroceso" onclick="termSendKey(8,'bsbutton')" style="display:none">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" id="pastebutton" value="Pegar" title="Pegar texto en la terminal" onclick="showTermPasteDialog()" style="display:none">
                                </div>
                                <div class="d-flex align-items-center ms-auto">
                                    <span id="TermTimer" title="Tiempo de sesión"></span>&nbsp;
                                    <span id="terminalSettingsButtons" style="display:none">
                                        <input id="id_tcrbutton" type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" value="CR+LF" title="Alternar lo que enviará la tecla Enter" onclick="termToggleCr()">
                                        <input id="id_tfxkeysbutton" type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" value="Intel (F10 = ESC+[OM)" title="Alternar el tipo de emulación con las teclas F1 a F10" onclick="termToggleFx()">
                                        <input id="id_ttypebutton" type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" value="Ascii extendido" title="Alternar el tipo de emulación de terminal" onclick="termToggleType()">
                                    </span>
                                    <span id="terminalSizeDropDown" style="display:none">
                                        <select id="termSizeList" onkeypress="return false" class="form-select-sm me-1">
                                            <option value="1">80x25</option>
                                            <option value="2">100x30</option>
                                        </select>
                                    </span>
                                    <select id="specialkeylist" class="form-select-sm me-1" onkeypress="return false"></select>
                                    <input id="specialkeylistinput" type="button" onkeypress="return false" class="btn btn-primary btn-sm me-1" value="Enviar" title="Enviar la clave especial seleccionada" onclick="sendSpecialKey()">
                                </div>
                            </td>
                        </tr>
                    </tbody></table>
                    <div id="p12TermConsoleMsg" style="display:none;text-align:left;cursor:pointer;position:absolute;left:30px;top:45px;color:yellow;background-color:rgba(0,0,0,0.6);padding:10px;border-radius:5px" onclick="p12clearConsoleMsg()"></div>
                </div>
            </div>
            <div id="p13" style="display:none">
                <div id="p13title" class="d-flex align-items-center">
                    <div id="p13BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-folder-open fa-sm me-2 text-secondary"></i>Archivos<span id="p13deviceName"></span></div>
                </div>
                <table id="p13toolbar" cellpadding="0" cellspacing="0">
                    <tbody><tr>
                        <td class="areaHead d-flex flex-wrap">
                            <div class="d-flex align-items-center">
                                <button id="p13AutoConnect" onclick="autoConnectFiles(event)" class="btn btn-primary btn-sm me-1" type="button" style="display:none"><i class="fa-solid fa-play"></i> AutoConectar</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connect" title="Conéctese usando el escritorio remoto de MeshAgent" onclick="connectFiles(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Conectar</button>
                                    <button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" id="p13Connectdrop" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">Alternar desplegable</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmconnectfilesaction()">Pedir Consentimiento</a></li>
                                    </ul>
                                </div>
                                <button id="p13Disconnect" class="btn btn-danger btn-sm me-1" onclick="connectFiles(event)" type="button">Desconectar</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectsspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connects" title="Conéctese usando el escritorio remoto de MeshAgent" onclick="connectFiles(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">Conexión SFTP</button>
                                    <button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" id="p13Connectsdrop" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">Alternar desplegable</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Alternar Puerto</a></li>
                                    </ul>
                                </div>
                                <span id="p13Status">Desconectado</span>
                            </div>
                            <div class="d-flex align-items-center ms-auto">
                                <input id="filesActionsBtn" class="btn btn-primary me-1 btn-sm" type="button" title="Realizar acciones de energía en el dispositivo" value="Acciones" onclick="deviceActionFunction()">
                                <div id="filesRecordIcon" class="deskareaicon" title="El servidor está grabando esta sesión" style="display:none;background-color:red;width:12px;height:12px;border-radius:6px;margin-top:5px;margin-left:5px">
                                </div>
                                <div id="filesCustomUpperRight" style="float:left;margin-right:6px"></div>
                                <div id="filesCustomUiButtons" style="float:left"></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td class="areaHead2 d-flex align-items-center flex-wrap" valign="bottom">
                            <div id="p13rightOfButtons" class="toright2 d-flex align-items-center"></div>
                            <div class="d-flex align-items-center">
                                <input type="button" disabled="disabled" id="p13FolderUp" class="btn btn-secondary me-1 btn-sm" value="Subir" onclick="p13folderup()">
                                <input type="button" disabled="disabled" id="p13SelectAllButton" class="btn btn-secondary me-1 btn-sm" value="Seleccionar Todo" onclick="p13selectallfile()">
                                <input type="button" disabled="disabled" id="p13RenameFileButton" class="btn btn-primary me-1 btn-sm" value="Renombrar" onclick="p13renamefile()">
                                <input type="button" disabled="disabled" id="p13DeleteFileButton" class="btn btn-primary me-1 btn-sm" value="Eliminar" onclick="p13deletefile()">
                                <input type="button" disabled="disabled" id="p13ViewFileButton" class="btn btn-primary me-1 btn-sm" value="Editar" onclick="p13viewfile()">
                                <input type="button" disabled="disabled" id="p13NewFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Nueva Carpeta" onclick="p13createfolder()">
                                <input type="button" disabled="disabled" id="p13NewFileButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Nuevo Archivo" onclick="p13createfile()">
                                <input type="button" disabled="disabled" id="p13UploadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Subir Archivo" onclick="p13uploadFile()">
                                <input type="button" disabled="disabled" id="p13DownloadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Descargar" onclick="p13downloadButton()">
                                <input type="button" disabled="disabled" id="p13CutButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Cortar" onclick="p13copyFile(1)">
                                <input type="button" disabled="disabled" id="p13CopyButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Copiar" onclick="p13copyFile(0)">
                                <input type="button" disabled="disabled" id="p13PasteButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Pegar" onclick="p13pasteFile()">
                                <input type="button" disabled="disabled" id="p13ZipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Comprimir en Zip" onclick="p13zipFiles()">
                                <input type="button" disabled="disabled" id="p13UnzipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Unzip" onclick="p13unzipFile()">
                                <input type="button" disabled="disabled" id="p13RefreshButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Actualizar" onclick="p13folderup(9999)">
                                <input type="button" disabled="disabled" id="p13FindButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Encontrar" onclick="p13findfile()">
                                <input type="button" disabled="disabled" id="p13GoToFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="GoTo" onclick="p13gotofolder()">
                                <input type="button" disabled="disabled" id="p13OpenButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Open" onclick="p13openfilefolder()">
                                <div class="btn-group me-1 mobileFileActionDropdown">
                                    <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">Más&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
                                    <div class="dropdown-menu">
                                        <button type="button" disabled="disabled" id="p13MobileNewFolderButton" class="dropdown-item" onclick="p13createfolder()">Nueva Carpeta</button>
                                        <button type="button" disabled="disabled" id="p13MobileNewFileButton" class="dropdown-item" onclick="p13createfile()">Nuevo Archivo</button>
                                        <button type="button" disabled="disabled" id="p13MobileUploadButton" class="dropdown-item" onclick="p13uploadFile()">Subir Archivo</button>
                                        <button type="button" disabled="disabled" id="p13MobileDownloadButton" class="dropdown-item" onclick="p13downloadButton()">Descargar</button>
                                        <button type="button" disabled="disabled" id="p13MobileCutButton" class="dropdown-item" onclick="p13copyFile(1)">Cortar</button>
                                        <button type="button" disabled="disabled" id="p13MobileCopyButton" class="dropdown-item" onclick="p13copyFile(0)">Copiar</button>
                                        <button type="button" disabled="disabled" id="p13MobilePasteButton" class="dropdown-item" onclick="p13pasteFile()">Pegar</button>
                                        <button type="button" disabled="disabled" id="p13MobileZipButton" class="dropdown-item" onclick="p13zipFiles()">Comprimir en Zip</button>
                                        <button type="button" disabled="disabled" id="p13MobileUnzipButton" class="dropdown-item" onclick="p13unzipFile()">Unzip</button>
                                        <button type="button" disabled="disabled" id="p13MobileRefreshButton" class="dropdown-item" onclick="p13folderup(9999)">Actualizar</button>
                                        <button type="button" disabled="disabled" id="p13MobileFindButton" class="dropdown-item" onclick="p13findfile()">Encontrar</button>
                                        <button type="button" disabled="disabled" id="p13MobileGoToFolderButton" class="dropdown-item" onclick="p13gotofolder()">GoTo</button>
                                        <button type="button" disabled="disabled" id="p13MobileOpenButton" class="dropdown-item" onclick="p13openfilefolder()">Open</button>
                                    </div>
                                </div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td class="areaHead3">
                            <div class="toright2">
                                <select id="p13sizedropdown" class="form-select-sm me-2" onchange="p13updateFiles()">
                                    <option value="0" selected="selected">Human Readable</option>
                                    <option value="1" title="Bytes">Bytes</option>
                                    <option value="10" title="KB">Kilobytes</option>
                                    <option value="20" title="MB">Megabytes</option>
                                    <option value="30" title="GB">Gigabyte</option>
                                </select>
                                <select id="p13sortdropdown" class="form-select-sm me-2" onchange="p13updateFiles()">
                                    <option value="1" selected="selected">Orden: ascendente, por nombre</option>
                                    <option value="2">Orden: ascendente, por tamaño</option>
                                    <option value="3">Orden: ascendente, por fecha</option>
                                    <option value="4">Orden: Descendente, por nombre</option>
                                    <option value="5">Orden: Descendente, por tamaño</option>
                                    <option value="6">Orden: Descendente, por fecha</option>
                                </select>
                            </div>
                            <div id="p13pathrow" style="display:none" ondblclick="p13pathRowClick(event)">&nbsp;&nbsp;<span id="p13currentpath" ondblclick="p13pathRowClick(event)"></span><input type="text" id="p13currentpathinput" class="form-control form-control-sm" style="width: auto; min-width: 400px; display:none;" onkeydown="return p13onPathKeyDown(event)" onblur="p13hidePathInput()"></div>
                        </td>
                    </tr>
                </tbody></table>
                <div id="p13FilesConsoleMsg" style="display:none;text-align:left;cursor:pointer;position:absolute;left:30px;top:165px;color:yellow;background-color:rgba(0,0,0,0.6);padding:10px;border-radius:5px" onclick="p13clearConsoleMsg()"></div>
                <div id="p13filetable" style="">
                    <div id="p13bigok" style="display:none"><b>✓</b></div>
                    <div id="p13bigfail" style="display:none"><b>✗</b></div>
                    <span id="p13files"></span>
                </div>
                <table id="p13toolbarBottom" cellpadding="0" cellspacing="0">
                    <tbody><tr>
                        <td class="style6">&nbsp;<span id="p13bottomstatus"></span></td>
                    </tr>
                </tbody></table>
            </div>
            <div id="p14" style="display:none">
                <div id="p14title" class="d-flex align-items-center">
                    <div id="p14BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><span id="p14deviceNamePrefix">Intel® AMT</span><span id="p14deviceName"></span></div>
                    <div id="devListToolbarViewIcons" class="ms-auto">
                        <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick="deskToggleFull(event)" title="Pantalla Completa. Mantenga presionada la tecla Mayús para que el navegador ocupe toda la pantalla."></i>
                    </div>
                </div>
                <iframe id="p14iframe" src="{{{domainurl}}}commander.ashx"></iframe>
            </div>
            <div id="p15" style="display:none">
                <div id="p15title" class="d-flex align-items-center">
                    <div id="p15BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Consola<span id="p15deviceName"></span></div>
                </div>
                <table id="consoleTable" cellpadding="0" cellspacing="0">
                    <tbody><tr>
                        <td class="areaHead d-flex flex-wrap">
                            <div id="p15statetext" class="d-flex align-items-center"></div>
                            <div class="d-flex align-items-center ms-auto">
                                <div id="p15coreName" class="me-1" title="Información sobre el núcleo actual que se ejecuta en este agente"></div>
                                <input type="button" id="p15uploadCore" class="btn btn-primary me-1 btn-sm" value="Acción del Agente" onclick="p15uploadCore(event)" title="Cambiar el módulo de código del agente JavaScript">
                                <i onclick="p15downloadConsoleText()" title="Descargar texto de consola" class="fa-solid fa-download me-1"></i>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <div class="areaProgress">
                                <div id="consoleprogressbar" style=""></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td id="p15agentConsole">
                            <pre id="p15agentConsoleText"></pre>
                        </td>
                    </tr>
                    <tr>
                        <td class="areaFoot">
                            <table style="width:100%">
                                <tbody><tr>
                                    <td style="width:99%">
                                        <input id="p15consoleText" class="form-control" onkeyup="p15consoleSend(event)" onfocus="onConsoleFocus(1)" onblur="onConsoleFocus(0)">
                                    </td>
                                    <td>&nbsp;</td>
                                    <td id="p15outputselecttd">
                                        <select id="p15outputselect" onchange="setupConsole()">
                                            <option id="p15outputselect1" value="1">Agente</option>
                                            <option id="p15outputselect3" value="3">Empujar</option>
                                            <option id="p15outputselect2" value="2">MQTT</option>
                                        </select>
                                    </td>
                                    <td style="width:1%"><input id="id_p15consoleClear" type="button" class="btn btn-primary bottombutton" value="Eliminar" onclick="p15consoleClear()"></td>
                                </tr>
                            </tbody></table>
                        </td>
                    </tr>
                </tbody></table>
            </div>
            <div id="p16" style="display:none">
                <div id="p16title" class="d-flex align-items-center">
                    <div id="p16BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-calendar fa-sm me-2 text-secondary"></i>Eventos<span id="p16deviceName"></span></div>
                </div>
                <table class="pTable">
                    <tbody><tr>
                        <td class="auto-style1 d-flex justify-content-end p-1">
                            <div class="d-flex align-items-center">
                                <label class="me-1" for="p16filterevents">Filtrar</label>
                                <select id="p16filterevents" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="">All Logs</option>
                                    <option notransval="1" value="agentlog">Agent Logs</option>
                                    <option notransval="1" value="relaylog">Relay Logs</option>
                                    <option notransval="1" value="manual">Manual Logs</option>
                                    <option notransval="1" value="runcommands">Run Command Logs</option>
                                    <option notransval="1" value="batchupload">Batch Upload Logs</option>
                                    <option notransval="1" value="changenode">Change Node Logs</option>
                                    <option notransval="1" value="removenode">Remove Node Logs</option>
                                </select>
                                <label class="me-1" for="p16limitdropdown">Mostrar</label>
                                <select id="p16limitdropdown" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="60">Últimos 60</option>
                                    <option notransval="1" value="120">Últimos 120</option>
                                    <option notransval="1" value="250">Últimos 250</option>
                                    <option notransval="1" value="500">Últimos 500</option>
                                    <option notransval="1" value="1000">Últimos 1000</option>
                                </select>
                                <i onclick="p3showDownloadEventsDialog(1)" class="fa-solid fa-download" title="Descargar Eventos" style="cursor:pointer"></i>
                            </div>
                        </td>
                    </tr>
                </tbody></table>
                <div id="p16events"></div>
            </div>
            <div id="p17" style="display:none;">
                <div id="p17title" class="d-flex align-items-center">
                    <div id="p17BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-list fa-sm me-2 text-secondary"></i>Detalles<span id="p17deviceName"></span></div>
                    <div id="devListToolbarViewIcons3" class="ms-auto">
                        <i class="fa-solid fa-rotate-right fa-xl fa-border" role="button" onclick="refreshDetails(event)" title="Actualice la información detallada."></i>
                        <i class="fa-solid fa-chart-line fa-xl fa-border" role="button" onclick="deskToggleCpuGraph(event)" title="Muestra el uso de la CPU y la memoria del dispositivo."></i>
                    </div>
                </div>
                <div id="p17info" class="ps-0 pb-5 overflow-y-auto container-fluid">
                    <div id="p17graph" style="width:100%">
                        <table style="width:100%">
                            <tbody><tr>
                                <td style="width:64px;vertical-align:top">
                                    <img src="images/details/graph64.png" border="0" width="64">
                                </td>
                                <td>
                                    <div class="DevSt" style="margin-bottom:3px;margin-left:16px"><b>Gráfico en vivo</b></div>
                                    <div style="margin-bottom:10px;margin-left:16px;height:240px;width:calc(100% - 16px);position:relative">
                                        <canvas id="deviceDetailsStats" style="position:absolute;top:0;left:0;right:0;bottom:0"></canvas>
                                    </div>
                                    <div id="extraGraphValues" style="display:none;margin-left:30px;margin-bottom:15px">
                                    </div>
                                </td>
                            </tr>
                        </tbody></table>
                    </div>
                    <div id="p17info2"></div>
                </div>
            </div>
            <div id="p18" style="display:none">
                <div id="p18title" class="d-flex align-items-center">
                    <div id="p18BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-cubes fa-sm me-2 text-secondary"></i>Software<span id="p18deviceName"></span></div>
                    <div id="devListToolbarViewIcons2" class="ms-auto">
                    </div>
                </div>
                <div id="p18toolbar" class="areaHead d-flex flex-wrap">
                    <div class="d-flex align-items-center">
                        <input type="button" id="p18RefreshBtn" class="btn btn-primary btn-sm me-1" value="Actualizar" onclick="loadInstalledApps()">
                        <input type="text" id="p18SearchInput" class="form-control-sm" placeholder="Buscar software..." style="width:250px" onkeyup="filterInstalledApps()">
                        <label style="margin-left:15px">
                            <input type="checkbox" id="p18ShowStore" onchange="toggleStoreApps()"> Mostrar aplicaciones de la tienda
                        </label>
                        <span id="p18Status" style="margin-left:20px"></span>
                    </div>
                </div>
                <div id="p18info" style="overflow-y:auto;padding:10px">
                    <div id="p18html">
                        <div style="text-align:center;padding:50px">
                            <i>Haga clic en Actualizar para cargar el software instalado...</i>
                        </div>
                    </div>
                </div>
		    </div>
            <div id="p19" style="display:none">
                <div id="p19title" class="d-flex align-items-center">
                    <div id="p19BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Plugins<span id="p19deviceName"></span></div>
                </div>
                <div id="p19headers"></div>
                <div id="p19pages"></div>
            </div>
            <div id="p20" style="display:none">
                <div id="p20main" style="overflow-y:auto">
                    <div id="p20title" class="d-flex align-items-center">
                        <div id="p20BackButton" class="pe-2">
                            <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                        </div>
                        <div class="fs-4 fw-bold">General<span id="p20meshName"></span></div>
                    </div>
                    <div class="d-flex">
                        <div class="flex-grow-1">
                            <div id="p20info3"></div>
                        </div>
                        <div>
                            <picture id="MainMeshImage">
                                <source type="image/webp" width="200" height="200" srcset="images/webp/mesh-256.webp">
                                <img alt="" width="200" height="200" src="images/mesh-256.png">
                            </picture>
                        </div>
                    </div>
                    <p id="p20info"></p>
                    <p id="p20info2"></p>
                </div>
            </div>
            <div id="p21" style="display:none">
                <div id="p21title" class="d-flex align-items-center">
                    <div id="p21BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Resumen - <span id="p21meshName"></span></div>
                </div>
                <div id="p21main" style="overflow-y:auto">
                    <div style="width:100%">
                        <div style="display:table;width:93%">
                            <div id="meshPowerChartDiv" style="width:23%;display:inline-block;text-align:center;max-width:250px">
                                <div style="margin:10px;font-size:16px">Estados de Encendido</div>
                                <canvas id="meshPowerChart" style="width:250px;height:250px"></canvas>
                            </div>
                            <div id="meshOsChartDiv" style="width:23%;display:inline-block;text-align:center;max-width:250px">
                                <div style="margin:10px;font-size:16px">Tipos de Agente</div>
                                <canvas id="meshOsChart" style="width:250px;height:250px"></canvas>
                            </div>
                            <div id="meshConnChartDiv" style="width:23%;display:inline-block;text-align:center;max-width:250px">
                                <div style="margin:10px;font-size:16px">Conectividad</div>
                                <canvas id="meshConnChart" style="width:250px;height:250px"></canvas>
                            </div>
                            <div id="meshSecurityChartDiv" style="width:23%;display:inline-block;text-align:center;max-width:250px">
                                <div style="margin:10px;font-size:16px">Seguridad</div>
                                <canvas id="meshSecurityChart" style="width:250px;height:250px"></canvas>
                            </div>
                        </div>
                    </div>
                    <p id="p21info" style="overflow-y:auto"></p>
                </div>
            </div>
            <div id="p30" style="display:none">
                <div id="p30title" class="d-flex align-items-center">
                    <div id="p30BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">General<span id="p30userName"></span></div>
                </div>
                <div id="p30info" class="pe-2" style="overflow-y:auto">
                    <div class="d-flex">
                        <div class="col">
                            <div id="p30html"></div>
                        </div>
                        <div class="col-auto">
                            <img id="p30userAuthServiceLogo" style="display:none" class="userAuthStrategyLogo" width="64" height="64">
                            <picture id="MainUserImage" style="display:none; border-width:0px; height:200px; width:200px; float:right" onclick="account_manageImage(1)">
                                <source type="image/webp" width="200" height="200" srcset="images/webp/user-256.webp">
                                <img alt="" width="200" height="200" src="images/user-256.png">
                            </picture>
                            <img id="MainUserImageEx" alt="" width="200" height="200" src="images/user-256.png" onclick="account_manageImage(1)">
                            <div style="width:100%;text-align:center;">
                                <strong><span id="MainUserState"></span></strong>
                            </div>
                        </div>
                    </div>
                    <br>


                    <div id="p30html2"></div>
                    <div id="p30html3"></div>
                </div>
            </div>
            <div id="p31" style="display:none">
                <div id="p31title" class="d-flex align-items-center">
                    <div id="p31BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Eventos<span id="p31userName"></span></div>
                </div>
                
                        {{!--  --}}
                        
                        {{!--  --}}
                    <table class="pTable">
                    <tbody><tr><td class="h1"></td><!--<td>&nbsp;<input type=button onclick=refreshUsersEvents() value="Refresh" /></td>-->
                        <td class="auto-style1 d-flex justify-content-end p-1">
                            <div class="d-flex align-items-center">
                                <label for="p31filterevents" class="me-1">Filtrar</label>
                                <select id="p31filterevents" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="">All Logs</option>
                                    <option notransval="1" value="agentlog">Agent Logs</option>
                                    <option notransval="1" value="relaylog">Relay Logs</option>
                                    <option notransval="1" value="manual">Manual Logs</option>
                                    <option notransval="1" value="runcommands">Run Command Logs</option>
                                    <option notransval="1" value="batchupload">Batch Upload Logs</option>
                                    <option notransval="1" value="changenode">Change Node Logs</option>
                                    <option notransval="1" value="removenode">Remove Node Logs</option>
                                </select>
                                <label for="p31limitdropdown" class="me-1">Mostrar</label>
                                <select id="p31limitdropdown" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="60">Últimos 60</option>
                                    <option notransval="1" value="120">Últimos 120</option>
                                    <option notransval="1" value="250">Últimos 250</option>
                                    <option notransval="1" value="500">Últimos 500</option>
                                    <option notransval="1" value="1000">Últimos 1000</option>
                                    <option notransval="1" value="">Sin límite</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(3)"><i class="fa-solid fa-download" role="button" title="Descargar Eventos"></i></a>
                            </div>
                        </td><td class="h2"></td></tr>
                </tbody></table>
                <div id="p31events" style=""></div>
            </div>
            <div id="p40" style="display:none">
                <div id="p40title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis Estadísticas del Servidor</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Actualizar" type="button" class="btn btn-primary btn-sm me-2" onclick="refreshServerTimelineStats()">
                        <div class="form-check"><input class="form-check-input me-1" type="checkbox" id="p40log" onclick="updateServerTimelineHours()"><label class="form-check-label" for="p40log">Log-X</label></div>
                    </div>
                    <div class="toright2 d-flex align-items-center ms-auto">
                        <select id="p40server" style="display:none" onchange="updateServerTimelineStats()"></select>
                        <select id="p40type" class="form-select-sm me-1" onchange="updateServerTimelineStats()">
                            <option value="0">Conexiones</option>
                            <option value="1">Memoria</option>
                            <option value="5">CPU</option>
                            <option value="3">Tráfico entrante</option>
                            <option value="4">Tráfico Saliente</option>
                        </select>
                        <select id="p40time" class="form-select-sm me-1" onchange="updateServerTimelineHours()">
                            <option value="3">Últimas 3 horas</option>
                            <option value="8">Últimas 8 horas</option>
                            <option value="24">Último día</option>
                            <option value="168">La semana pasada</option>
                            <option value="720">Últimos 30 días</option>
                        </select>
                        <i class="fa-solid fa-download" title="Descargar puntos de datos (.csv)" style="cursor:pointer" onclick="p40downloadEvents()"></i>
                    </div>
                </div>
                <canvas id="serverMainStats" style=""></canvas>
            </div>
            <div id="p41" style="display:none">
                <div id="p41title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mi Seguimiento del Servidor</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Rastreo" type="button" class="btn btn-primary btn-sm me-1" onclick="setServerTracing()">
                        <span id="p41traceStatus">Ninguno</span>
                    </div>
                    <div class="toright2 d-flex align-items-center ms-auto">
                        <label for="p41limitdropdown" class="me-1">Mostrar</label>
                        <select id="p41limitdropdown" class="form-select-sm me-1" onchange="displayServerTrace()">
                            <option value="100">Últimos 100</option>
                            <option value="250">Últimos 250</option>
                            <option value="500">Últimos 500</option>
                            <option value="1000">Últimos 1000</option>
                        </select>
                        <input value="Eliminar" type="button" class="btn btn-primary btn-sm me-1" onclick="clearServerTracing()">
                        <i class="fa-solid fa-download" title="Descargar trace (.csv)" style="cursor:pointer" onclick="p41downloadServerTrace()"></i>
                    </div>
                </div>
                <div id="p41events" style=""></div>
            </div>
            <div id="p42" style="display:none">
                <div id="p42title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis Plugins de Servidor</div>
                </div>
                <div class="areaHead">
                    <div class="toright2">
                    </div>
                    <div>
                        <input value="Descargar Plugin" type="button" onclick="return pluginHandler.addPluginDlg();">
                    </div>
                </div>
                <div id="pluginRestartNotice" class="areaHead" style="background-color:gold;display:none">
                    <div class="toright2">
                        <input value="Actualizar Núcleos de Agente" type="button" onclick="distributeCore();return false">
                    </div>
                    <div style="padding:2px">
                        <div style="padding:2px"><b>Aviso:</b> Se han modificado los plugins, esto puede requerir la actualización del núcleo del agente.</div>
                    </div>
                </div>
                <table id="p42tbl">
                    <tbody><tr class="DevSt">
                        <th style="width:26px"></th>
                        <th style="width:10px"></th>
                        <th class="chName">Nombre</th>
                        <th class="chDescription">Descripción</th>
                        <th class="chSite" style="text-align:center">Enlace</th>
                        <th class="chVersion" style="text-align:center">Versión</th>
                        <th class="chUpgradeAvail" style="text-align:center">Última</th>
                        <th class="chStatus" style="text-align:center">Estado</th>
                        <th class="chAction" style="text-align:center">Acción</th>
                        <th style="width:10px"></th>
                    </tr>
                </tbody></table>
                <div id="pluginNoneNotice" style="width:100%;text-align:center;padding-top:10px;display:none"><i>No hay plugins en el servidor.</i></div>
            </div>
            <div id="p43" style="display:none">
                <div id="p43BackButton">
                    <div class="backButton" tabindex="0" onmouseup="go(42)" title="Atrás" onkeypress="if (event.key == 'Enter') go(42)">
                        <div class="backButtonEx"></div>
                    </div>
                </div>
                <h1>Mis Plugins de Servidor - <span id="p43title"></span></h1>
                <iframe id="p43iframe" frameborder="0" style="width:100%;height:calc(100vh - 245px);max-height:calc(100vh - 245px)"></iframe>
            </div>
            <div id="p50" style="display:none">
                <div id="p50title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis Grupos de Usuarios</div>
                </div>
                <table class="pTable">
                    <tbody><tr>
                        <td class="style14 d-flex align-items-center flex-wrap p-1">
                            <div id="p50userGroupOps" class="d-flex align-items-center">
                                <input type="button" id="UsersGroupsSelectAllButton" class="btn btn-secondary btn-sm me-1" onclick="p50usersSelectallButtonFunction()" value="Seleccionar Todo">
                                <input type="button" id="UsersGroupsGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Acción de Grupo" onclick="p50usersGroupActionFunction()">
                                <input id="NewUserGroupButton" type="button" class="btn btn-success btn-sm me-1" onclick="showCreateUserGroupDialog(1)" value="Nuevo Grupo...">
                                <input id="DuplicateUserGroupButton" type="button" class="btn btn-primary button-outline-success btn-sm me-1" style="display:none" onclick="showCreateUserGroupDialog(2)" value="Duplicar Grupo...">
                            </div>
                        </td>
                    </tr>
                </tbody></table>
                <div id="p50groups"></div>
            </div>
            <div id="p51" style="display:none">
                <div id="p51title" class="d-flex align-items-center">
                    <div id="p51BackButton" class="pe-2">
                        <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Atrás" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Grupo de Usuarios - <span id="p51groupName"></span></div>
                </div>
                <div id="p51info" style="overflow-y:auto">
                    <div class="row">
                        <div class="col">
                            <div id="p51group"></div>
                        </div>
                        <div class="col-auto" style="width: 20px;"></div>
                        <div class="col-auto" style="width: 200px;">
                            <picture id="MainUserImage" style="border-width:0px;height:200px;width:200px;float:right">
                                <source type="image/webp" width="200" height="200" srcset="images/webp/group-256.webp">
                                <img alt="" width="200" height="200" src="images/group-256.png">
                            </picture>
                            <div style="width:100%;text-align:center">
                                <strong><span id="MainUserState"></span></strong>
                            </div>
                        </div>
                    </div>
                    <div id="p51group2"></div>
                    <br>
                </div>
            </div>
            <div id="p52" style="display:none">
                <div id="p52title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis Grabaciones de Usuarios</div>
                </div>
                <table class="pTable">
                    <tbody><tr>
                        <td class="style14 d-flex align-items-center flex-wrap p-1">
                            <div class="d-flex align-items-center">
                                <input type="button" class="btn btn-primary btn-sm" onclick="refreshRecodings()" value="Actualizar">
                            </div>
                            <div class="d-flex align-items-center ms-auto">
                                <input type="button" class="btn btn-primary btn-sm" onclick="openRecodringPlayer()" value="Abrir Reproductor...">
                            </div>
                        </td>
                    </tr>
                </tbody></table>
                <div id="p52recordings" style="overflow-y:auto"></div>
            </div>
            <div id="p60" style="display:none">
                <div id="p60title" class="d-flex align-items-center">
                    <div class="fs-4 fw-bold">Mis informes</div>
                </div>
                
                        {{!--  --}}
                        
                        {{!--  --}}
                    <table class="pTable">
                    <tbody><tr><td class="h1"></td><td class="style14">
                            <div style="float:right;line-height:22px">
                                <a id="p60downloadReportDiv" style="display:none" href="#" onclick="p60downloadReport()"><i class="fa-solid fa-download" title="Descargar Reporte"></i></a>&nbsp;
                            </div>
                            <div>
                                <button class="btn btn-primary btn-sm me-2 m-1" onclick="generateReportDialog()"><i class="fa-regular fa-file-alt"></i> Generar informe...</button>
                            </div>
                        </td><td class="h2"></td></tr>
                </tbody></table>
                <div id="p60report" style="overflow-y:auto"></div>
            </div>
            <br id="column_l_bottomgap">
        </div>
        <div id="footer">
            <div class="footer1">{{{footer}}}</div>
            <div class="footer2">
                <div class="row">
                    <div class="col-md-6 d-flex gap-2">
                    </div>
                    <div class="col-md-6">
                        <a id="verifyEmailId2" style="display:none" href="#" onclick="account_showVerifyEmail()">Verificar Correo Electrónico</a>
                        &nbsp;<a id="termsLinkFooter" href="terms">Términos y Privacidad</a>
                    </div>
                </div>
            </div>
        </div>
        <div class="modal fade" id="xxAddAgentModal" tabindex="-1" aria-hidden="true">
            <div class="modal-dialog modal-dialog-centered" id="xxAddAgentModalConf">
                <div class="modal-content">
                    <div class="modal-header" id="dialogHeader">
                        <h5 class="modal-title text-white" id="xxAddAgentTitle"></h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body" id="xxAddAgentBody">
                        <!-- The default dialog is 2 but possible to specify others -->
                        <div id="dialog1">
                            <div id="id_dialogMessage" style=""></div>
                        </div>
                        <div id="dialog2" style="">
                            <div id="id_dialogOptions"></div>
                        </div>
                        <div id="dialog3" style="">
                            <div id="d3upload">
                                <div>Selección de Archivo</div>
                                <select id="d3uploadMode" onchange="d3modechange()">
                                    <option value="1">Carga local de archivos</option>
                                    <option value="2">Seleccionar un archivo del servidor</option>
                                </select>
                            </div>
                            <div id="d3localmode" style="display:none">
                                <div>Cargar Archivo</div>
                                <form id="d3localmodeform" method="post" enctype="multipart/form-data" action="uploadfile.ashx" target="fileUploadFrame">
                                    <input type="text" id="d3auth" name="auth" style="display:none">
                                    <input type="text" id="d3filter" name="filter" style="display:none">
                                    <input type="text" id="d3attrib" name="attrib" style="display:none">
                                    <input type="file" id="d3localFile" name="files" onchange="d3setActions()">
                                    <input type="submit" id="d3submit" style="display:none">
                                </form>
                            </div>
                            <div id="d3servermode">
                                <div id="d3serveraction" valign="bottom">
                                    <input type="button" id="p3FolderUp" disabled="disabled" onclick="d3folderup()" value="Subir">&nbsp;<span id="p3CurrentFolder"></span>
                                </div>
                                <div id="d3serverfiles"></div>
                            </div>
                        </div>
                        <div id="dialog4" style="">
                            <input id="d4WrapButton" class="btn btn-primary me-1 mb-1" type="button" value="Envoltura" onclick="d4ToggleWrap()">
                            <input id="d4SizeButton" class="btn btn-primary me-1 mb-1" type="button" value="Pequeño" onclick="d4ToggleSize()">
                            <input id="d4EncodingButton" class="btn btn-primary me-1 mb-1" type="button" value="Crudo" onclick="d4ToggleEncoding()">
                            <input id="d4LineBreakButton" class="btn btn-primary me-1 mb-1" type="button" value="Windows" onclick="d4ToggleLineBreak()">
                            <textarea id="d4editorarea" autocomplete="off" style="height:calc(100vh - 286px);width:100%;overflow:scroll;resize:none;white-space:pre"></textarea>
                        </div>
                        <div id="dialog7" class="tab-content" style="">
                            <div class="nav nav-pills mb-3">
                                <button id="td7meshkvm" class="nav-link" onclick="changeDesktopSettingsTab(event, 'd7meshkvm')">Agente</button>
                                <button id="td7rdpkvm" class="nav-link" onclick="changeDesktopSettingsTab(event, 'd7rdpkvm')">RDP</button>
                                <button id="td7amtkvm" class="nav-link" onclick="changeDesktopSettingsTab(event, 'd7amtkvm')">Intel® AMT</button>
                            </div>
                            <div id="d7meshkvm" class="tab-pane">
                                <!--<h4>Agent Remote Desktop</h4>-->
                                <div class="form-floating mb-2">
                                    <select id="d7bitmapquality" class="form-select"></select>
                                    <label for="d7bitmapquality">Calidad</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7bitmapscaling" class="form-select">
                                        <option selected="selected" value="1024">100%</option>
                                        <option value="896">87.5%</option>
                                        <option value="768">75%</option>
                                        <option value="640">62.5%</option>
                                        <option value="512">50%</option>
                                        <option value="384">37.5%</option>
                                        <option value="256">25%</option>
                                        <option value="128">12.5%</option>
                                    </select>
                                    <label for="d7bitmapscaling">Escala</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7framelimiter" class="form-select">
                                        <option selected="selected" value="50">Rápido</option>
                                        <option value="100">Medio</option>
                                        <option value="400">Lento</option>
                                        <option value="1000">Muy lento</option>
                                    </select>
                                    <label for="d7framelimiter">Cuadros por segundo</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7encoding" class="form-select">
                                        <option value="1">JPEG</option>
                                        <option value="2">PNG</option>
                                        <option value="3">TIFF</option>
                                        <option selected="selected" value="4">WEBP</option>
                                    </select>
                                    <label for="d7encoding">Codificación</label>
                                </div>
                                <fieldset id="d7desktopOtherSettings">
                                    <legend>Otros Ajustes</legend>
                                    <div id="d7otherset2" class="p-2">
                                        <div class="form-check">
                                            <input id="d7deskSwapMouse" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskSwapMouse" class="form-check-label">Cambiar Botones del Mouse</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskrmw" class="form-check-label">Rueda inversa del ratón</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskRemoteKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskRemoteKeyMap" class="form-check-label">Usar Mapa de Teclado Remoto</label>
                                        </div>
                                        <div id="d7deskAutoClipboardLabel" class="form-check">
                                            <input id="d7deskAutoClipboard" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskAutoClipboard" class="form-check-label">Portapapeles Automático</label>
                                        </div>
                                        <div id="d7deskAutoLockLabel" class="form-check">
                                            <input id="d7deskAutoLock" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskAutoLock" class="form-check-label">Bloquear al desconectar</label>
                                        </div>
                                    </div>
                                </fieldset>
                            </div>
                            <div id="d7amtkvm" class="tab-pane">
                                <!--<h4>Intel&reg; AMT Hardware KVM</h4>-->
                                <div class="form-floating mb-2">
                                    <select id="d7desktopmode" class="form-select">
                                        <option value="1">RLE8, Más Rápido</option>
                                        <option value="2">RLE16, Recomendado</option>
                                        <option value="3">RAW8, Lento</option>
                                        <option value="4">RAW16, Muy Lento</option>
                                    </select>
                                    <label for="d7desktopmode">Codificación de Imagen</label>
                                </div>
                                <fieldset>
                                    <legend>Otros Ajustes</legend>
                                    <div id="d7otherset" class="p-2">
                                        <div class="form-check">
                                            <input id="d7showfocus" class="form-check-input me-2" type="checkbox">
                                            <label for="d7showfocus" class="form-check-label">Mostrar Herramienta de Enfoque</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7showcursor" class="form-check-input me-2" type="checkbox">
                                            <label for="d7showcursor" class="form-check-label">Mostrar Cursor del Mouse Local</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7localKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7localKeyMap" class="form-check-label">Mapa de Teclado Local</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7kvmrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7kvmrmw" class="form-check-label">Rueda inversa del ratón</label>
                                        </div>
                                    </div>
                                </fieldset>
                            </div>
                            <div id="d7rdpkvm" class="tab-pane">
                                <!--<h4>Remote Desktop Protocol</h4>-->
                                <div class="form-floating mb-2">
                                    <select id="d7rdpsize" class="form-select">
                                        <option notransval="1" value="canvas">Tamaño del lienzo</option>
                                        <option notransval="1" value="browser">Tamaño del navegador</option>
                                        <option notransval="1" value="screen">Tamaño de pantalla</option>
                                        <option notransval="1" value="640x480">640x480</option>
                                        <option notransval="1" value="1024x768">1024x768</option>
                                        <option notransval="1" value="1280x800">1280x800</option>
                                        <option notransval="1" value="1440x900">1440x900</option>
                                        <option notransval="1" value="1600x900">1600x900</option>
                                        <option notransval="1" value="1680x1050">1680x1050</option>
                                        <option notransval="1" value="1920x1080">1920x1080</option>
                                    </select>
                                    <label for="d7rdpsize">tamaño de la pantalla</label>
                                </div>
                                <fieldset>
                                    <legend>Opciones</legend>
                                    <div id="d7rdpflags" class="p-2">
                                        <div class="form-check">
                                            <input id="d7rdp1" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp1" class="form-check-label">Desactivar fondo de pantalla</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp2" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp2" class="form-check-label">Desactivar arrastre de ventana completa</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp3" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp3" class="form-check-label">Desactivar animaciones de menú</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp4" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp4" class="form-check-label">Deshabilitar tematización</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp6" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp6" class="form-check-label">Deshabilitar la sombra del cursor</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp7" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp7" class="form-check-label">Deshabilitar la configuración del cursor</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp8" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp8" class="form-check-label">Habilitar suavizado de fuentes</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp9" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp9" class="form-check-label">Habilitar composición de escritorio</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpclip" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpclip" class="form-check-label">Portapapeles Automático</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpsmb" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpsmb" class="form-check-label">Cambiar Botones del Mouse</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdprmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdprmw" class="form-check-label">Rueda inversa del ratón</label>
                                        </div>
                                    </div>
                                </fieldset>
                            </div>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="idx_dlgCancelButton">Cancelar</button>
                        <button type="button" class="btn btn-primary" id="idx_dlgOkButton">OK</button>
                        <button type="button" class="btn btn-danger" id="idx_dlgDeleteButton" style="display:none">Eliminar</button>
                        <!-- <div id="idx_dlgMoreButtons">
                            <a href="#" id="idx_dlgMoreButtons1" class="btn btn-primary"
                                title="Toggle advanced options" onclick="MoreToggle(true)">&#x25BC;</a>
                            <a href="#" id="idx_dlgMoreButtons2" class="btn btn-primary"
                                title="Toggle advanced options" onclick="MoreToggle(false)">&#x25B2;</a>
                        </div> -->
                    </div>
                </div>
            </div>
        </div>
        <iframe name="fileUploadFrame" style="display:none"></iframe>
        <form style="display:none" method="post" action="uploadfile.ashx" enctype="multipart/form-data" target="fileUploadFrame"><input id="p5fileDragName" name="name"><input id="p5fileDragAuthCookie" name="auth"><input id="p5fileDragSize" name="size"><input id="p5fileDragType" name="type"><input id="p5fileDragData" name="data"><input id="p5fileDragLink" name="link" class="btn btn-outline-success"><input type="submit" id="p5loginSubmit2" style="display:none" class="btn btn-outline-success"></form>
        <form style="display:none" method="post" action="uploadnodefile.ashx" enctype="multipart/form-data" target="fileUploadFrame"><input id="p13fileDragName" name="name" class="btn btn-outline-success"><input id="p13fileDragSize" name="size" class="btn btn-outline-success"><input id="p13fileDragType" name="type" class="btn btn-outline-success"><input id="p13fileDragData" name="data" class="btn btn-outline-success"><input id="p13fileDragLink" name="link" class="btn btn-outline-success"><input type="submit" id="p13loginSubmit2" style="display:none" class="btn btn-outline-success"></form>


        <audio id="chimes"><source src="sounds/chimes.mp3" type="audio/mp3"></audio>
        <iframe style="display:none" name="fileDownloadFrame"></iframe>
    </div>
    <script type="text/javascript">
        'use strict';
        var random = '{{{randomlength}}}' // Random length string for BREACH mitigation

        // Process server-side web state
        var webState = '{{{webstate}}}';
        if (webState != '') { webState = JSON.parse(decodeURIComponent(webState)); }
        if ((webState == null) || (typeof webState != 'object')) { webState = {}; }
        for (var i in webState) { try { localStorage.setItem(i, webState[i]); } catch (ex) { } }
        if (webState && !webState.loctag) { try { delete localStorage.removeItem('loctag'); } catch (ex) { } }

        var args, urlargs;
        var autoReconnect = true;
        var powerStatetable = ['', "Encendido", "Suspender", "Suspender", "Suspender", "Hibernando", "Apagar", "Presente"];
        var StatusStrs = ["Desconectado", "Conectando....", "Configurar...", "Conectado", "Intel&reg; AMT Conectado"];
        var agentsStr = ["Desconocido", "Consola Windows 32bits", "Consola Windows 64bits", "Servicio Windows 32bits", "Servicio Windows 64bits", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86-32bit", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86-32bit", "macOS x86-64bit", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Consola MinCore de Windows", "Servicio MinCore de Windows", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bits", "ARMv6l / ARMv7l / Sin KVM", "MIPS24KC (OpenWRT)", "Procesadores Apple", "FreeBSD x86-64", "Desconocido", "Linux ARM 64 bit", "Alpine Linux x86 64 Bit (MUSL)", "Asistente (Windows)", "Armada370 - ARM32 / HF (libc / 2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Desconocido", "Desconocido", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Consola Windows ARM 64bit", "Servicio Windows ARM 64bit", "ARMVIRT32 (OpenWRT)", "RISC-V 64bit"];
        var agentsStrNoAgent = ['', '', '', '', "Windows", '', "Linux", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', "Apple"];
        var domainStates = ["Ninguno", "Azure AD", "On-Premises AD", "Hybrid AD", "Microsoft Account"];
        var sort = 0;
        var searchFocus = 0;
        var mapSearchFocus = 0;
        var userSearchFocus = 0;
        var consoleFocus = 0;
        var showRealNames = false;
        var meshserver = null;
        var meshes = {};
        var loginTokens = {};
        var meshcount = 0;
        var nodes = null;
        var usergroups = null;
        var filetree = {};
        var userinfo = null;
        var serverinfo = null;
        var events = [];
        var users = null;
        var wssessions = null;
        var stars = {}; // Devices that have been "stared" by the user.
        var nodeShortIdent = 0;
        var desktop;
        var desktopsettings = { encoding: 2, showfocus: false, showmouse: true, showcad: true, quality: 40, scaling: 1024, framerate: 50, localkeymap: false, swapmouse: false, remotekeymap: false, autoclipboard: false, autolock: false, agentencoding: 4 };
        var multidesktopsettings = { quality: 20, scaling: 128, framerate: 1000, agentencoding: 4 };
        var terminal;
        var files;
        var debugLevel = parseInt('{{{debuglevel}}}');
        var features = parseInt('{{{features}}}');
        var features2 = parseInt('{{{features2}}}');
        var features3 = parseInt('{{{features3}}}');
        var sessionTime = parseInt('{{{sessiontime}}}');
        var webRelayPort = parseInt('{{{webRelayPort}}}');
        var webRelayDns = '{{{webRelayDns}}}';
        var hidePowerTimeline = '{{{hidePowerTimeline}}}';
        var showNotesPanel = '{{{showNotesPanel}}}';
        var sessionRefreshTimer = null;
        var domain = '{{{domain}}}';
        var domainUrl = '{{{domainurl}}}';
        var authCookie = '{{{authCookie}}}';
        var authRelayCookie = '{{{authRelayCookie}}}';
        var logoutControls = JSON.parse(decodeURIComponent('{{{logoutControls}}}'));
        var authCookieRenewTimer = null;
        var multiDesktop = {};
        var serverPublicNamePort = '{{{serverDnsName}}}:{{{serverPublicPort}}}';
        var amtScanResults = null;
        var debugmode = 0;
        var windowsBrowser = detectWindowsBrowser();
        var attemptWebRTC = ((features & 128) != 0);
        var webrtcconfiguration = '{{{webrtcconfig}}}';
        if (webrtcconfiguration == '') { webrtcconfiguration = null; } else { try { webrtcconfiguration = JSON.parse(decodeURIComponent(webrtcconfiguration)); } catch (ex) { console.log('Invalid WebRTC config: "' + webrtcconfiguration + '".'); webrtcconfiguration = null; } }
        var passRequirements = '{{{passRequirements}}}';
        if (passRequirements != '') { passRequirements = JSON.parse(decodeURIComponent(passRequirements)); }
        var customui = '{{{customui}}}';
        if (customui != '') { customui = JSON.parse(decodeURIComponent(customui)); } else { customui = null; }
        var deskAspectRatio = 0;
        try { deskAspectRatio = parseInt(getstore('deskAspectRatio', '0')); } catch (ex) { }
        var uiMode = parseInt(getstore('uiMode', 1));
        var webPageStackMenu = false;
        var webPageFullScreen = true;
        var nightMode = setNightMode();
        var footerBar = (getstore('footerBar', '1') == '1');
        var sessionActivity = Date.now();
        var updateSessionTimer = null;
        var pluginHandlerBuilder = {{{pluginHandler}}};
        var pluginHandler = null;
        if (pluginHandlerBuilder != null) { pluginHandler = new pluginHandlerBuilder(); }
        var installedPluginList = null;
        var goBackStack = [];
        var CollapsedGroups = {};
        var collapseGroups = '{{{collapseGroups}}}';
        try { CollapsedGroups = JSON.parse(getstore('_collapse', '{}')); } catch (ex) { }
        var deviceViewSettings = {};
        try { deviceViewSettings = JSON.parse(getstore('_deviceViewSettings', '{}')); } catch (ex) { }
        var userViewSettings = {};
        try { userViewSettings = JSON.parse(getstore('_usersViewSettings', '{}')); } catch (ex) { }
        var xterm = null;
        var xtermfit = null;
        var xtermimage = null;
        var xtermResizeTimer = null;
        var miscState = {};
        var checkedNodeids = {};
        var deskKeyboardShortcuts = [];
        var deskKeyboardStrings = [];
        var deskLastClipboardSent = null;
        var deskLastClipboardReceived = null;
        var requestedLastConnects = false;
        var devicePagingState = null;

        // Console Message Display Timers
        var p11DeskConsoleMsgTimer = null;
        var p12TermConsoleMsgTimer = null;
        var p13FilesConsoleMsgTimer = null;

        /*
        // Check browser dark mode preference
        var prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
        var prefersLightScheme = window.matchMedia('(prefers-color-scheme: light)').matches;
        console.log(prefersDarkScheme, prefersLightScheme);
        var mql = window.matchMedia('(prefers-color-scheme: dark)');
        mql.addEventListener('change', function() { console.log('Dark Change'); });
        */

        // Check if WebP is supported
        var webpSupport = false;
        check_webp_feature('lossy', function (f, x) {
            webpSupport = x;
            if (!x) {
                d7encoding.options[1].disabled = true;
                d7encoding.value = 1;
            }
        });

        function startup() {
            if ((features & 32) == 0) {
                // Guard against other site's top frames (web bugs).
                var loc = null;
                try { loc = top.location.toString().toLowerCase(); } catch (e) { }
                if (top != self && (loc == null || top.active == false)) { top.location = self.location; return; }
            }

            // Fetch URL arguments & do sanitation
            urlargs = parseUriArgs();
            if (urlargs.key != null) { urlargs.key = "" + urlargs.key; }
            if (urlargs.key && (isAlphaNumeric(urlargs.key) == false)) { delete urlargs.key; }
            if (urlargs.locale && (isAlphaNumeric(urlargs.locale) == false)) { delete urlargs.locale; }
            delete urlargs.user;
            delete urlargs.pass;
            delete urlargs.viewmode;
            delete urlargs.gotonode;
            delete urlargs.gotodevicename;
            delete urlargs.gotodevicername;
            delete urlargs.gotodeviceip;
            delete urlargs.gotomesh;
            delete urlargs.gotouser;
            delete urlargs.gotougrp;

            // Fix links if a loginKey is used
            if (urlargs.key) { Q('termsLinkFooter').href += '?key=' + urlargs.key; }

            // Check if we are in debug mode
            args = parseUriArgs();
            if (args.key && (isAlphaNumeric(args.key) == false)) { delete args.key; }
            if (args.locale && (isAlphaNumeric(args.locale) == false)) { delete args.locale; }

            if (!args.locale) { var x = getstore('loctag', 0); if ((x != null) && (x != '*')) { args.locale = x; } }
            debugmode = args.debug;

            //attemptWebRTC = false; // For now, default WebRTC off unless we set it in the URL.
            if (args.webrtc != null) { attemptWebRTC = (args.webrtc == 1); }

            QV('p13AutoConnect', debugmode); // Files
            QV('autoconnectbutton2', debugmode); // Terminal
            QV('autoconnectbutton1', debugmode); // Desktop
            //QV('DeskClip', debugmode); // Clipboard feature, not completed so show in in debug mode only.

            if (nightMode) {
                document.documentElement.setAttribute('data-bs-theme', 'dark');
                QC('body').add('night'); QS('body')['background-color'] = '#000';
            }
            toggleFullScreen();

            // Setup stared devices
            try { stars = JSON.parse(getstore('stars', '{}')); } catch (ex) { }

            // Setup page visuals
            var hide = 0;
            var globalHide = parseInt('{{{hide}}}');
            if (globalHide || args.hide) {
                if (args.hide) { hide = parseInt(args.hide); }
                if (globalHide) { hide = (hide | globalHide); }
            }
            args.hide = hide;
            QV('uiViewButton5', !(args.hide & 4)); // Hide the footer toggle button if footer is hidden anyway.
            QV('toggleFooterMenuItem', !(args.hide & 4)); // Hide the footer toggle menu item if footer is hidden anyway.
            QV('toggleNightMenuItem', ((features2 & 0x00100000) == 0) && ((features2 & 0x00200000) == 0)); // Hide night mode toggle if permanent night (1) or day (2) mode is set
            //QV('toggleModernUIMenuItem', ((features2 & 0x80000000) == 0)); // Show only if allowed and not forced siteStyle=3
            updateMobileTopBarVisibility();
            adjustPanels();

            setTimeout(updateUserDropdownVisibility, 100);

            // Setup the context menu
            document.onclick = function (e) { hideContextMenu(); }
            document.onkeypress = ondockeypress;
            document.onkeydown = ondockeydown;
            document.onkeyup = ondockeyup;
            //window.addEventListener('focus', ondocfocus, false);
            window.addEventListener('blur', ondocblur, false);
            window.onresize = function () {
                hideContextMenu();
                mainUpdate(512);
                browserfullscreen = isBrowserFullscreen();
                QV('DeskESC', browserfullscreen);
                if ((xtermfit != null) && (xxcurrentView == 12)) { xtermfit.fit(); }
            }
            setTimeout(function () {
                if (urlargs.filter) { Q('SearchInput').value = urlargs.filter; Q('KvmSearchInput').value = urlargs.filter; }
                mainUpdate(512);
            }, 200);

            // open notes markdown links in new tab
            if (DOMPurify) {
                DOMPurify.addHook('afterSanitizeAttributes', function (node) {
                    if ('target' in node) {
                        node.setAttribute('target', '_blank');
                        node.setAttribute('rel', 'noopener noreferrer');
                    }
                    if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {
                        node.setAttribute('xlink:show', 'new');
                    }
                });
            }

            // Show the modern ui switcher
            QV('textnewui', ((features2 & 0x40000000) == 0) ? false : true);

            // Show Connect All and Disconnect All
            QV('KvmDesktopButtons', ((features3 & 0x00000001) == 0) ? true : false);

            // Connect to the mesh server
            meshserver = MeshServerCreateControl(domainUrl);
            meshserver.onStateChanged = onStateChanged;
            meshserver.onMessage = onMessage;
            meshserver.trace = args.trace;
            meshserver.Start();

            // Setup page controls
            Q('sortselect').selectedIndex = sort = getstore('sort', 0);
            Q('sizeselect').selectedIndex = getstore('viewsize', 1);
            Q('KvmSearchInput').value = Q('SearchInput').value = getstore('_search', '');
            showRealNames = (getstore('showRealNames', 0) == 1);
            Q('RealNameCheckBox').checked = showRealNames;
            Q('DevFilterSelect').value = getstore('devFilterSelect', 0);
            Q('viewselect').value = getstore('deviceView', 1);
            Q('DeskControl').checked = (getstore('DeskControl', 1) == 1);
            QV('accountChangeEmailAddressSpan', (features & 0x200000) == 0);

            // Display the page devices
            mainUpdate(3)
            for (var j = 1; j < 5; j++) { Q('devViewButton' + j).classList.remove('viewSelectorSel'); }
            Q('devViewButton' + Q('viewselect').value).classList.add('viewSelectorSel');

            // Setup upload drag & drop for server files
            Q('p5filetable').addEventListener('drop', p5fileDragDrop, false);
            Q('p5filetable').addEventListener('dragover', p5fileDragOver, false);
            Q('p5filetable').addEventListener('dragleave', p5fileDragLeave, false);
            //Q('p5fileCatchAllInput').addEventListener('drop', p5fileDragDrop, false);
            //Q('p5fileCatchAllInput').addEventListener('dragover', p5fileDragOver, false);
            //Q('p5fileCatchAllInput').addEventListener('dragleave', p5fileDragLeave, false);

            // Setup drag & drop for device desktop
            Q('deskarea0').addEventListener('drop', p11fileDragDrop, false);
            Q('deskarea0').addEventListener('dragover', p11fileDragOver, false);
            Q('deskarea0').addEventListener('dragleave', p11fileDragLeave, false);

            // Setup upload drag & drop for device files
            Q('p13filetable').addEventListener('drop', p13fileDragDrop, false);
            Q('p13filetable').addEventListener('dragover', p13fileDragOver, false);
            Q('p13filetable').addEventListener('dragleave', p13fileDragLeave, false);

            // Timeline update interval
            setInterval(updateDeviceTimeline, 120000); // Check every 2 minutes

            // Load desktop settings
            var t = null;
            try { t = localStorage.getItem('desktopsettings'); } catch (ex) { }
            if (t != null) { desktopsettings = JSON.parse(t); }
            t = null;
            try { t = localStorage.getItem('multidesktopsettings'); } catch (ex) { }
            if (t != null) { multidesktopsettings = JSON.parse(t); }
            applyDesktopSettings();

            // Terminal special keys
            var x = '';
            for (var c = 1; c < 27; c++) x += '<option value=\'' + c + '\'>' + "Ctrl" + '-' + String.fromCharCode(64 + c) + ' (' + c + ')</option>';
            QH('specialkeylist', x);

            // Setup server stats panels
            setupGeneralServerStats();
            setupServerTimelineStats();

            // Setup the user interface in the right mode
            userInterfaceSelectMenu();

            // Setup Mesh summary panel
            setupMeshSummaryStats();

            // If SSPI or LDAP authentication not used, allow batch account creation.
            QV('p4UserBatchCreate', (features & 0x00080000) == 0);
            QV('p4MobileUserBatchCreate', (features & 0x00080000) == 0);

            // Set the file editor
            d4EditWrapVal = Number(getstore('editorWrap', 0));
            d4EditSizeVal = Number(getstore('editorSize', 0));
            d4EditEncodingVal = Number(getstore('editorEncoding', 0));
            d4EditLineBreakVal = Number(getstore('editorLineBreak', 0));
            d4ToggleWrap(true);
            d4ToggleSize(true);
            d4ToggleEncoding(true);
            d4ToggleLineBreak(true);
            if (pluginHandler != null) pluginHandler.callHook('onWebUIStartupEnd');

            // Deleted non-english style and fix all topbar titles
            if ((['en', 'ko', 'ja', 'zh-chs', 'zh-cht'].indexOf('{{{lang}}}') == -1) && ('{{{lang}}}'.toLowerCase() != '')) { QC('body').add('nonenglish'); }
            var elements = document.getElementsByClassName('topbar_td');
            for (var i in elements) { if (elements[i].innerHTML) { elements[i].innerHTML = elements[i].innerHTML.split(' ').join('&nbsp;'); } }

            // Custom UI
            if (customui != null) {
                if (customui.devicesbarbuttons) {
                    var x = '&nbsp;';
                    for (var i in customui.devicesbarbuttons) {
                        var disabled = ((customui.devicesbarbuttons[i].selection == 'one') || (customui.devicesbarbuttons[i].selection == 'many')) ? 'disabled' : '';
                        x += '<input id="cui:' + i + '" type="button" class="btn btn-secondary" value="' + customui.devicesbarbuttons[i].name + '" ' + disabled + ' onclick=customUIAction(event,"devicesbarbuttons") />&nbsp;';
                    }
                    QH('devsCustomUIBar', x);
                }
                if (customui.desktopbuttons) {
                    var x = '&nbsp;';
                    for (var i in customui.desktopbuttons) { x += '<input id="cui:' + i + '" type="button" class="btn btn-secondary" value="' + customui.desktopbuttons[i].name + '" style="float:left" onclick=customUIAction(event,"desktopbuttons") />&nbsp;'; }
                    QH('desktopCustomUiButtons', x);
                }
                if (customui.terminalbuttons) {
                    var x = '&nbsp;';
                    for (var i in customui.terminalbuttons) { x += '<input id="cui:' + i + '" type="button" class="btn btn-secondary" value="' + customui.terminalbuttons[i].name + '" style="float:left" onclick=customUIAction(event,"terminalbuttons") />&nbsp;'; }
                    QH('terminalCustomUiButtons', x);
                }
                if (customui.filesbuttons) {
                    var x = '&nbsp;';
                    for (var i in customui.filesbuttons) { x += '<input id="cui:' + i + '" type="button" class="btn btn-secondary" value="' + customui.filesbuttons[i].name + '" style="float:left" onclick=customUIAction(event,"filesbuttons") />&nbsp;'; }
                    QH('filesCustomUiButtons', x);
                }
            }

            // Session Refresh Timer
            if (sessionTime >= 10) { sessionRefreshTimer = setTimeout(refreshCookieSession, Math.round((sessionTime * 60000) * 0.8)); }

            // Set the user's desktop shortcut keys
            deskKeyboardShortcuts = [];
            var deskKeyboardShortcutsStr = getstore('deskKeyShortcuts', '0x0A002E,0x100000,0x100028,0x100026,0x10004C,0x10004D,0x11004D,0x100052,0x020073,0x080057,0x020009,0x100025,0x100027').split(',');
            for (var i in deskKeyboardShortcutsStr) { if (deskKeyboardShortcutsStr[i] != "") { deskKeyboardShortcuts.push(parseInt(deskKeyboardShortcutsStr[i])); } }
            updateDeskShortcutKeys();

            // Set the user's desktop strings
            try { deskKeyboardStrings = JSON.parse(getstore('deskStrings', '[]')); } catch (ex) { }
            updateDesktopStrings();

            // Override the collapse button text
            updateCollapseAllButton();

            // Make the dialog box movable
            dialogBoxDrag();

            // Hide night mode button if needed
            QV('uiViewButton4', (features2 & 0x00300000) == 0);

            // Fix HTML words that in english have two or more meanings
            Q('DeskType').value = multiTranslate("[KeyboardTyping]|Tipo");

            // Show the "Scroll to top" button
            QV('ScrollToTopButton', (features2 & 0x08000000) != 0);
        }

        function refreshCookieSession() {
            var xdr = null;
            try { xdr = new XDomainRequest(); } catch (e) { }
            if (!xdr) xdr = new XMLHttpRequest();
            xdr.open('GET', window.location.origin + domainUrl + 'refresh.ashx');
            xdr.timeout = 15000;
            xdr.onload = function () { sessionRefreshTimer = setTimeout(refreshCookieSession, Math.round((sessionTime * 60000) * 0.8)); };
            xdr.onerror = xdr.ontimeout = function () { sessionRefreshTimer = null; };
            xdr.send();
        }

        // Generic handling of custom actions
        function customUIAction(e, section) {
            var id = e.srcElement.id;
            if (id.startsWith('cui:') == false) return;
            var info = customui[section][id.substring(4)];
            if (info == null) return;
            if (section == 'devicesbarbuttons') {
                var elements = document.getElementsByClassName('DeviceCheckbox'), selectedDevices = [];
                for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { selectedDevices.push(elements[i].defaultValue.substring(6)); } }
                if (typeof info.action == 'string') {
                    if (info.action == 'event') { meshserver.send({ action: 'uicustomevent', section: section, element: id.substring(4), selectedDevices: selectedDevices, logmsg: info.logmsg }); }
                    if (info.action.startsWith('dialog:')) { showCustomUiDialog(info.action.substring(7), { section: section, element: id.substring(4), selectedDevices: selectedDevices }); }
                    if (info.deselectdevices) { for (var i = 0; i < elements.length; i++) { elements[i].checked = false; } checkedNodeids = {}; p1updateInfo(); }
                }
            }
            if ((section == 'devicebuttons') || (section == 'desktopbuttons') || (section == 'terminalbuttons') || (section == 'filesbuttons')) {
                var selectedDevices = [currentNode._id];
                if (typeof info.action == 'string') {
                    if (info.action == 'event') { meshserver.send({ action: 'uicustomevent', section: section, element: id.substring(4), selectedDevices: selectedDevices, logmsg: info.logmsg }); }
                    if (info.action.startsWith('dialog:')) { showCustomUiDialog(info.action.substring(7), { section: section, element: id.substring(4), selectedDevices: selectedDevices }); }
                }
            }
        }

        // Display a generic custom UI dialog
        function showCustomUiDialog(name, tag) {
            if ((customui == null) || (customui.dialogs == null) || (typeof customui.dialogs[name] != 'object')) return;
            var x = '', dialog = customui.dialogs[name], buttons = 3;
            if (typeof dialog.text == 'string') { x += '<div style=margin-bottom:8px>' + dialog.text + '</div>'; }
            if (typeof dialog.buttons == 'number') { buttons = dialog.buttons; }
            if (typeof dialog.elements == 'object') {
                for (var i in dialog.elements) {
                    var elem = dialog.elements[i];
                    if (elem.type == 'text') { x += addHtmlFormFloating(elem.name, '<input id=cui:' + i + ' class=form-control autocomplete=off />'); }
                    if (elem.type == 'textarea') {
                        if (elem.name == null) {
                            x += '<textarea id=cui:' + i + ' style=width:356px;resize:none;height:100px autocomplete=off></textarea>';
                        } else {
                            x += addHtmlFormFloating(elem.name, '<textarea class=form-control id=cui:' + i + ' style=resize:none;height:100px autocomplete=off></textarea>');
                        }
                    }
                    if (elem.type == 'droplist') {
                        var y = '';
                        for (var j in elem.options) { y += '<option value="' + j + '">' + EscapeHtml(elem.options[j]) + '</option>'; }
                        x += addHtmlFormFloating(elem.name, '<select class=form-select id="cui:' + i + '" class=form-select autocomplete=off />' + y + '</select>');
                    }
                }
            }
            setModalContent('xxAddAgent', dialog.title, x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showCustomUiDialogEx(1, { action: 'uicustomevent', section: 'dialogs', element: name, src: tag, values: {}, logmsg: dialog.logmsg }) );
        }

        // Handle a generic custom UI dialog event
        function showCustomUiDialogEx(b, t) {
            if (b != 1) return;
            var dialog = customui.dialogs[t.element];
            if (typeof dialog.elements == 'object') {
                for (var i in dialog.elements) {
                    var elem = dialog.elements[i];
                    if ((elem.type == 'droplist') || (elem.type == 'textarea') || (elem.type == 'text')) { t.values[i] = Q('cui:' + i).value; }
                }
            }
            meshserver.send(t);
            if (dialog.deselectdevices) {
                var elements = document.getElementsByClassName('DeviceCheckbox')
                for (var i = 0; i < elements.length; i++) { elements[i].checked = false; } checkedNodeids = {}; p1updateInfo();
            }
        }

        function adjustPanels() {
            var hide = args.hide;
            if (footerBar == false) { hide |= 4; }
            QV('masthead', !(hide & 1));
            QV('topbar', !(hide & 2));
            QV('footer', !(hide & 4));
            if ((hide & 8)) {
                QC('p1title').add('d-none');
                QC('p2title').add('d-none');
                QC('p3title').add('d-none');
                QC('p4title').add('d-none');
                QC('p5title').add('d-none');
                QC('p6title').add('d-none');
                QC('p10title').add('d-none');
                QC('p11title').add('d-none');
                QC('p12title').add('d-none');
                QC('p13title').add('d-none');
                QC('p14title').add('d-none');
                QC('p15title').add('d-none');
                QC('p16title').add('d-none');
                QC('p17title').add('d-none');
                QC('p18title').add('d-none');
                QC('p20title').add('d-none');
                QC('p21title').add('d-none');
                QC('p30title').add('d-none');
                QC('p31title').add('d-none');
                QC('p40title').add('d-none');
                QC('p41title').add('d-none');
                QC('p50title').add('d-none');
                QC('p51title').add('d-none');
                QC('column_l').remove('pt-3');
            }
            //if (hide & 16) { QV('page_leftbar', false); QS('page_content').left = '0px'; }
            if (!footerBar) { QC('body').add('nofooter'); } else { QC('body').remove('nofooter'); }

            if (args.hide != 0) {
                // Fix the main grid to zero-height elements we want to hide.
                if (uiMode == 2) {
                    QS('container')['grid-template-rows'] = ((hide & 1) ? '0' : '66') + 'px fit-content(48px) auto ' + ((hide & 4) ? '0' : '45') + 'px';
                    QS('container')['-ms-grid-rows'] = ((hide & 1) ? '0' : '66') + 'px fit-content(48px) auto ' + ((hide & 4) ? '0' : '45') + 'px';
                } else {
                    QS('container')['grid-template-rows'] = ((hide & 1) ? '0' : '66') + 'px ' + ((hide & 2) ? '0' : '24') + 'px auto ' + ((hide & 4) ? '0' : '45') + 'px';
                    QS('container')['-ms-grid-rows'] = ((hide & 1) ? '0' : '66') + 'px ' + ((hide & 2) ? '0' : '24') + 'px auto ' + ((hide & 4) ? '0' : '45') + 'px';
                }
            }

            // Adjust height of remote desktop, files and Intel AMT
            // 1 = Top bar, 2 = Tool Bar, 4 = Bottom Bar, 8 = Tab Title
            var xh = (((hide & 1) ? 0 : 66) + ((hide & 2) ? 0 : 24) + ((hide & 4) ? 0 : 45) + ((hide & 8) ? 0 : 60)); // 0 to 195
            var xh2 = (uiMode > 1) ? 24 : 0;
            QS('p2info')['height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p2info')['max-height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p3users')['max-height'] = 'calc(100vh - ' + (50 + xh) + 'px)'; // 124
            QS('p50groups')['max-height'] = 'calc(100vh - ' + (50 + xh) + 'px)'; // 124
            QS('p3events')['height'] = 'calc(100vh - ' + (50 + xh) + 'px)'; // 124
            QS('p5filetable')['height'] = 'calc(100vh - ' + (119 + xh) + 'px)'; // 140
            QS('p13filetable')['height'] = 'calc(100vh - ' + (127 + xh + xh2) + 'px)'; // 124
            QS('serverMainStats')['height'] = 'calc(100vh - ' + (50 + xh + xh2) + 'px)'; // 110
            QS('serverMainStats')['max-height'] = 'calc(100vh - ' + (50 + xh + xh2) + 'px)'; // 110
            QS('xdevices')['max-height'] = 'calc(100vh - ' + (46 + xh) + 'px)'; // 124
            QS('xdevicesmap')['max-height'] = 'calc(100vh - ' + (46 + xh) + 'px)'; // 124
            QS('p15agentConsole')['height'] = 'calc(100vh - ' + (84 + xh + xh2) + 'px)';
            QS('p15agentConsole')['max-height'] = 'calc(100vh - ' + (84 + xh + xh2) + 'px)';
            QS('p15agentConsoleText')['height'] = 'calc(100vh - ' + (81 + xh + xh2) + 'px)';
            QS('p15agentConsoleText')['max-height'] = 'calc(100vh - ' + (81 + xh + xh2) + 'px)';
            var xtermActive = !((args.xterm === 0) || ((terminal != null) && (xterm == null)));
            if (fullscreen) {
                QS('deskarea3x')['height'] = null;
                QS('deskarea3x')['max-height'] = null;
                QS('p14iframe')['height'] = null;
                QS('p14iframe')['max-height'] = null;
                if (xtermActive) {
                    QS('termarea3x')['height'] = 'calc(100vh - 55px)';
                    QS('termarea3xdiv')['height'] = 'calc(100vh - 55px)';
                    QS('termarea3x')['max-width'] = 'calc(1px)';
                }
            } else {
                QS('deskarea3x')['height'] = 'calc(100vh - ' + (74 + xh + xh2) + 'px)';
                QS('deskarea3x')['max-height'] = 'calc(100vh - ' + (74 + xh + xh2) + 'px)';
                QS('p14iframe')['height'] = 'calc(100vh - ' + (23 + xh + xh2) + 'px)';
                QS('p14iframe')['max-height'] = 'calc(100vh - ' + (23 + xh + xh2) + 'px)';
                if (xtermActive) {
                    QS('termarea3x')['height'] = 'calc(100vh - ' + (74 + xh + xh2) + 'px)';
                    QS('termarea3xdiv')['height'] = 'calc(100vh - ' + (74 + xh + xh2) + 'px)';
                    QS('termarea3x')['max-width'] = 'calc(1px)';
                }
            }
            QS('p43iframe')['height'] = 'calc(100vh - ' + (84 + xh) + 'px)';
            QS('p43iframe')['max-height'] = 'calc(100vh - ' + (84 + xh) + 'px)';
            QS('p6info')['height'] = 'calc(100vh - ' + (-50 + xh + xh2) + 'px)';
            QS('p6info')['max-height'] = 'calc(100vh - ' + (-50 + xh + xh2) + 'px)';
            QS('p10info')['height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p10info')['max-height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p17info')['height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p17info')['max-height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p18info')['height'] = 'calc(100vh - ' + (120 + xh + xh2) + 'px)';
			QS('p18info')['max-height'] = 'calc(100vh - ' + (120 + xh + xh2) + 'px)';
            QS('p20main')['height'] = 'calc(100vh - ' + (-50 + xh + xh2) + 'px)';
            QS('p20main')['max-height'] = 'calc(100vh - ' + (-50 + xh + xh2) + 'px)';
            QS('p21main')['height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p21main')['max-height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p30info')['height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p30info')['max-height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p51info')['height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p51info')['max-height'] = 'calc(100vh - ' + (20 + xh + xh2) + 'px)';
            QS('p16events')['height'] = 'calc(100vh - ' + (50 + xh + xh2) + 'px)';
            QS('p16events')['max-height'] = 'calc(100vh - ' + (50 + xh + xh2) + 'px)';
            QS('p31events')['height'] = 'calc(100vh - ' + (50 + xh + xh2) + 'px)';
            QS('p31events')['max-height'] = 'calc(100vh - ' + (50 + xh + xh2) + 'px)';
            QS('p41events')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
            QS('p41events')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
            QS('p52recordings')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
            QS('p52recordings')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
            QS('p60report')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
            QS('p60report')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';

            // We are looking at a single device, remove all the back buttons
            if ((args.hide & 32) || ('{{currentNode}}'.toLowerCase() != '')) {
                QV('p10BackButton', false);
                QV('p11BackButton', false);
                QV('p12BackButton', false);
                QV('p13BackButton', false);
                QV('p14BackButton', false);
                QV('p15BackButton', false);
                QV('p16BackButton', false);
                QV('p17BackButton', false);
                QV('p18BackButton', false);
            }
            p1updateInfo();
        }

        // Toggle the web page to full screen
        function toggleAspectRatio(toggle) {
            if (toggle === 1) { deskAspectRatio = ((deskAspectRatio + 1) % 3); putstore('deskAspectRatio', deskAspectRatio); }
            deskAdjust();
        }

        // If FullScreen, toggle menu to be horizontal or vertical
        function toggleStackMenu(toggle) {
            if (webPageFullScreen == true) {
                if (toggle === 1) {
                    webPageStackMenu = !webPageStackMenu;
                    putstore('webPageStackMenu', webPageStackMenu);
                }
                if (webPageStackMenu == false) {
                    QC('body').remove('menu_stack');
                } else {
                    QC('body').add('menu_stack');
                    if (xxcurrentView >= 10) QC('column_l').remove('room4submenu');
                }
                deskAdjust();
            }
        }

        // Toggle user interface menu
        function showUserInterfaceSelectMenu() {
            if (xxdialogMode) return;
            Q('uiViewButton1').classList.remove('uiSelectorSel');
            Q('uiViewButton2').classList.remove('uiSelectorSel');
            Q('uiViewButton3').classList.remove('uiSelectorSel');
            Q('uiViewButton4').classList.remove('uiSelectorSel');
            Q('uiViewButton5').classList.remove('uiSelectorSel');
            try { Q('uiViewButton' + uiMode).classList.add('uiSelectorSel'); } catch (ex) { }
            QV('uiMenu', (QS('uiMenu').display == 'none'));
            //Q('uiViewButton1').focus();
            if (nightMode) { Q('uiViewButton4').classList.add('uiSelectorSel'); }
            if (footerBar) { Q('uiViewButton5').classList.add('uiSelectorSel'); }
        }

        // Toggle user dropdown menu
        function toggleUserDropdown() {
            if (xxdialogMode) return;
            var userDropdownMenu = Q('userDropdownMenu');
            if (userDropdownMenu) {
                var isVisible = (userDropdownMenu.style.display == 'none' || userDropdownMenu.style.display == '');
                if (isVisible) {
                    userDropdownMenu.style.display = 'block';
                    QV('uiMenu', false);
                    QV('topMenu', false);
                    updateNightModeIcon();
                    updateMobileTopBarVisibility();
                } else {
                    userDropdownMenu.style.display = 'none';
                    var uiSubmenu = Q('uiSubmenu');
                    if (uiSubmenu) {
                        uiSubmenu.style.display = 'none';
                    }
                    resetChevronArrow();
                }
            }
        }
        function updateUserDropdown() {
            if (logoutControls && logoutControls.name) {
                var userImageSrc = 'images/user-256.png';
                if (userinfo && userinfo.flags && (userinfo.flags & 1)) {
                    if (userinfo.accountImageRnd == null) { userinfo.accountImageRnd = Math.floor(Math.random() * 9999999999); }
                    userImageSrc = 'userimage.ashx?rnd=' + userinfo.accountImageRnd;
                }

                var userImage = Q('userDropdownImage');
                if (userImage) {
                    userImage.src = userImageSrc;
                }

                var userDropdownButton = Q('userDropdownButton');
                if (userDropdownButton) {
                    userDropdownButton.onclick = function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        toggleUserDropdown();
                        return false;
                    };
                }

                var userDropdownMenu = Q('userDropdownMenu');
                if (userDropdownMenu) {
                    userDropdownMenu.style.display = 'none';
                }
            }
        }

        function toggleUISubmenu(event) {
            if (event) {
                event.stopPropagation();
                event.preventDefault();
            }
            var uiSubmenu = Q('uiSubmenu');
            if (uiSubmenu) {
                var isVisible = (uiSubmenu.style.display == 'block');

                if (isVisible) {
                    uiSubmenu.style.display = 'none';
                    uiSubmenu.classList.remove('show');
                    var chevronIcon = document.querySelector('.userDropdownUISettings .fa-chevron-right');
                    if (chevronIcon) {
                        chevronIcon.classList.remove('rotated');
                    }
                    document.removeEventListener('click', closeUISubmenu);
                } else {
                    uiSubmenu.style.display = 'block';
                    uiSubmenu.classList.add('show');
                    var chevronIcon = document.querySelector('.userDropdownUISettings .fa-chevron-right');
                    if (chevronIcon) {
                        chevronIcon.classList.add('rotated');
                    }
                    setTimeout(function() {
                        document.addEventListener('click', closeUISubmenu);
                    }, 10);
                }
            }
        }

        function closeUISubmenu(event) {
            var uiSubmenu = Q('uiSubmenu');
            var userDropdown = Q('userDropdown');
            if (uiSubmenu && userDropdown) {
                if (!uiSubmenu.contains(event.target) && !userDropdown.contains(event.target)) {
                    uiSubmenu.style.display = 'none';
                    uiSubmenu.classList.remove('show');
                    var chevronIcon = document.querySelector('.userDropdownUISettings .fa-chevron-right');
                    if (chevronIcon) {
                        chevronIcon.classList.remove('rotated');
                    }
                    document.removeEventListener('click', closeUISubmenu);
                }
            }
        }

        function userInterfaceSelectMenu(s) {
            if (s) { uiMode = s; putstore('uiMode', uiMode); }
            webPageFullScreen = (uiMode < 3);
            webPageStackMenu = (uiMode > 1);
            toggleFullScreen(0);
            toggleStackMenu(0);
            if (webPageStackMenu && (xxcurrentView >= 10)) { QC('column_l').add('room4submenu'); } else { QC('column_l').remove('room4submenu'); }
            updateMobileTopBarVisibility();
            adjustPanels();
        }

        function updateMobileTopBarVisibility() {
            try {
                var items = document.querySelectorAll('.mobile-menu-item');
                for (var i = 0; i < items.length; i++) {
                    if ((uiMode == 2) || (uiMode == 3)) {
                        items[i].classList.add('d-none');
                    } else {
                        if (!(items[i].classList.contains('users-menu-item') || items[i].classList.contains('files-menu-item') || items[i].classList.contains('server-menu-item'))) {
                            items[i].classList.remove('d-none');
                        }
                    }
                }

                if ((uiMode != 2) && (uiMode != 3)) {
                    var serverFeatures = parseInt('{{{serverfeatures}}}');
                    var siteRights = userinfo.siteadmin;

                    // Server Menu Item
                    ((siteRights & 21) && ((serverFeatures & 64) != 0)) ? QC('mobileServerMenuItem').remove('d-none') : QC('mobileServerMenuItem').add('d-none');

                    // Files Menu Item
                    (siteRights & 8) ? QC('mobileFilesMenuItem').remove('d-none') : QC('mobileFilesMenuItem').add('d-none');

                    // Users Menu Item
                    (((users != null) && ((features & 4) == 0)) || (((userinfo.siteadmin & 512) != 0) && ((features & 0x08000000) != 0))) ? QC('mobileUsersMenuItem').remove('d-none') : QC('mobileUsersMenuItem').add('d-none');
                }
            } catch (ex) { }
        }

        function toggleNightMode() {
            if (xxdialogMode) return;
            var cNightMode = getstore('nightMode', '0');
            var x = '<input type=radio id=night0 name=nightmoderadio value=0 ' + ((cNightMode == 0) ? 'checked' : '') + '><label for=night0>' + "Predeterminado del navegador" + '</label><br>';
            x += '<input type=radio id=night2 name=nightmoderadio value=2 ' + ((cNightMode == 2) ? 'checked' : '') + '><label for=night2>' + "Modo de luz" + '</label><br>';
            x += '<input type=radio id=night1 name=nightmoderadio value=1 ' + ((cNightMode == 1) ? 'checked' : '') + '><label for=night1>' + "Modo Oscuro" + '</label><br>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Modo Nocturno", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', toggleNightModeEx);

            QV('uiMenu', false);
        }

        function toggleNightModeEx() {
            // Save new night mode
            var nNightMode = '0';
            if (Q('night1').checked) { nNightMode = '1'; }
            if (Q('night2').checked) { nNightMode = '2'; }
            putstore('nightMode', nNightMode);
            setNightMode();
        }

        if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setNightMode); }

        function setNightMode() {
            // Set night mode
            var nNightMode = getstore('nightMode', '0')
            nightMode = false;
            if ((features2 & 0x00100000) != 0) { nNightMode = '1'; }
            if ((features2 & 0x00200000) != 0) { nNightMode = '2'; }
            if (nNightMode == '1') { nightMode = true; }
            else if ((nNightMode == '0') && (window.matchMedia)) { nightMode = window.matchMedia('(prefers-color-scheme: dark)').matches }
            if (nightMode) {
                document.documentElement.setAttribute('data-bs-theme', 'dark');
                QC('body').add('night'); QS('body')['background-color'] = '#000';
            } else {
                document.documentElement.setAttribute('data-bs-theme', 'light');
                QC('body').remove('night'); QS('body')['background-color'] = '#d3d9d6';
            }
            return nightMode;
        }

        function toggleFooterBarMode() {
            footerBar = !footerBar;
            putstore('footerBar', footerBar ? '1' : '0');
            QS('container')['grid-template-rows'] = null;
            QS('container')['-ms-grid-rows'] = null;
            adjustPanels();
        }

        // Toggle the web page to full screen
        function toggleFullScreen(toggle) {
            const leftBar = document.getElementById('page_leftbar');
            if (toggle === 1) { webPageFullScreen = !webPageFullScreen; putstore('webPageFullScreen', webPageFullScreen); }
            if (webPageFullScreen == false) {
                QC('body').remove('menu_stack');
                QC('body').remove('fullscreen');
                QC('body').remove('arg_hide');
                if (xxcurrentView >= 10) QC('column_l').add('room4submenu');
                leftBar.classList.add('d-none');
                //QV('page_leftbar', false);
            } else {
                QC('body').add('fullscreen');
                if (args.hide & 16) QC('body').add('arg_hide');
                QV('page_leftbar', !(args.hide & 16));
                QV('MainMenuSpan', !(args.hide & 16));
                if (xxcurrentView >= 10) QC('column_l').remove('room4submenu');
                leftBar.classList.remove('d-none');
            }
            mainUpdate(512);
            QV('body', true);
        }

        function saveUserInterfaceMode() {
            var nUiViewMode = 3;
            if (Q('ui0').checked) { nUiViewMode = 2; }
            if (getstore('uiViewMode', 3) != nUiViewMode) {
                putstore('uiViewMode', nUiViewMode);
                // Check if the URL includes 'sitestyle=' and remove it
                var url = new URL(window.location.href);
                if (url.searchParams.has('sitestyle')) {
                    url.searchParams.delete('sitestyle');
                    window.history.replaceState({}, document.title, url.toString());
                }
                reload();
            }
        }

        function toggleBootstrapUIMode() {
            if (xxdialogMode) return;
            var uiViewMode = getstore('uiViewMode', 3);
            var x = '<div class=form-check><input type=radio class=form-check-input id=ui0 name=uiradio value=2 ' + ((uiViewMode == 2)?'checked':'') + '><label class=form-check-label for=ui0>' + "Classic" + '</label></div>';
            x += '<div class=form-check><input type=radio class=form-check-input id=ui1 name=uiradio value=3 ' + ((uiViewMode == 3)?'checked':'') + '><label class=form-check-label for=ui1>' + "Modern" + '</label></div>';
            setModalContent('xxAddAgent', "Interfaz de usuario", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', saveUserInterfaceMode);
            QV('uiMenu', false);
        }

        function getNodeFromId(id) { if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == id) return nodes[i]; } } return null; }
        function reload() {
            var x = window.location.href;
            if (x.endsWith('/#')) { x = x.substring(0, x.length - 2); }
            if (x.endsWith('#')) { x = x.substring(0, x.length - 1); }
            window.location.href = x;
        }

        function onStateChanged(server, state, prevState, errorCode) {
            if (state == 0) {
                // Control web socket disconnected
                setDialogMode(0); // Close any dialog boxes if present
                go(0); // Go to disconnection panel

                // Clean up
                powerTimeline = null;
                powerTimelineReq = null;
                powerTimelineNode = null;
                powerTimelineUpdate = null;
                deviceShares = null;
                deleteAllNotifications(); // Close and clear notifications if present
                hideContextMenu(); // Hide the context menu if present
                QV('verifyEmailId2', false);
                QV('logoutControl', false);
                if (errorCode == 'noauth') { QH('p0span', "No se puede realizar la autenticación"); return; }
                if (errorCode == 'invalidorigin') { QH('p0span', "Invalid origin in HTTP request"); return; }
                if (prevState == 2) { if (autoReconnect) { setTimeout(serverPoll, 5000); } } else { QH('p0span', "No se puede conectar al web socket"); }
                if (authCookieRenewTimer != null) { clearInterval(authCookieRenewTimer); authCookieRenewTimer = null; }
            } else if (state == 2) {
                // Fetch list of meshes, nodes, files
                meshserver.send({ action: 'usergroups' });
                meshserver.send({ action: 'meshes' });
                meshserver.send({ action: 'nodes', id: '{{currentNode}}', skip: (devicePagingState == null) ? 0 : devicePagingState.skip });
                meshserver.send({ action: 'loginTokens' });
                if (pluginHandler != null) { meshserver.send({ action: 'plugins' }); }
                if ('{{currentNode}}'.toLowerCase() == '') { meshserver.send({ action: 'files' }); }
                if ('{{viewmode}}'.toLowerCase() == '') { go(1); }
                authCookieRenewTimer = setInterval(function () { meshserver.send({ action: 'authcookie' }); }, 1800000); // Request a cookie refresh every 30 minutes.
                if (xxcurrentView == 40) { refreshServerTimelineStats(); }
            }
        }

        // Poll the server, if it responds, refresh the page.
        function serverPoll() {
            var xdr = null;
            try { xdr = new XDomainRequest(); } catch (e) { }
            if (!xdr) xdr = new XMLHttpRequest();
            xdr.open('HEAD', window.location.href);
            xdr.timeout = 15000;
            // Make sure there isn't a reverse proxy in front that may just be returning 5xx codes
            // Status code 4xx should still be allowed, since a page could potentially be removed, etc
            xdr.onload = function () { if (xdr.status < 500) reload(); else setTimeout(serverPoll, 10000); };
            xdr.onerror = xdr.ontimeout = function () { setTimeout(serverPoll, 10000); };
            xdr.send();
        }

        // Return true if this browser is Windows based
        function detectWindowsBrowser() {
            var userAgent = window.navigator.userAgent.toUpperCase();
            return (userAgent.indexOf('WINDOWS') >= 0) || (userAgent.indexOf('WIN32') >= 0) || (userAgent.indexOf('WIN64') >= 0);
        }

        function updateSiteAdmin() {
            if (!userinfo) return;
            var serverFeatures = parseInt('{{{serverfeatures}}}');
            var siteRights = userinfo.siteadmin;
            var accountSettingsLocked = ((siteRights != 0xFFFFFFFF) && ((siteRights & 1024) != 0)) || ((features2 & 0x100) != 0); // Not admin and have account features locked, or using a loginToken

            // Update account actions
            QV('p2AccountSecurity', ((features & 4) == 0) && (serverinfo.domainauth == false) && ((features & 4096) != 0) && (accountSettingsLocked == false)); // Hide Account Security if in single user mode or domain authentication, 2 factor auth not supported.
            QV('p2AccountActions', !accountSettingsLocked)
            QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000) && (serverinfo.lock2factor != true));
            QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000) && (serverinfo.lock2factor != true));
            QV('manageMessaging1', (features2 & 0x02000000) && (features2 & 0x04000000) && (serverinfo.lock2factor != true));
            QV('manageMessaging2', (features2 & 0x02000000) && !(features2 & 0x04000000) && (serverinfo.lock2factor != true));
            QV('manageEmail2FA', (features & 0x00800000) && (serverinfo.lock2factor != true));
            QV('p2AccountPassActions', ((features & 4) == 0) && (serverinfo.domainauth == false) && (userinfo != null) && (userinfo._id.split('/')[2].startsWith('~') == false)); // Hide Account Actions if in single user mode or domain authentication
            //QV('p2AccountImage', ((features & 4) == 0) && (serverinfo.domainauth == false)); // If account actions are not visible, also remove the image on that panel
            QV('accountCreateLoginTokenSpan', features2 & 0x00000080);
            QV('p2AccountImageFrame', !accountSettingsLocked)
            QV('p2ServerActions', (siteRights & 21) && ((serverFeatures & 143) != 0));
            QV('LeftMenuMyServer', (siteRights & 21) && ((serverFeatures & 64) != 0)); // 16 + 4 + 1
            QV('MainMenuMyServer', siteRights & 21);
            QV('accountCustomIconsSpan', true);
            QV('p2ServerActionsBackup', (siteRights & 1) && ((serverFeatures & 1) != 0));
            QV('p2ServerActionsRestore', (siteRights & 4) && ((serverFeatures & 2) != 0));
            QV('p2ServerActionsVersion', (siteRights & 16) && ((serverFeatures & 4) != 0));
            QV('p2ServerActionsErrors', (siteRights & 16) && ((serverFeatures & 8) != 0));
            QV('p2ServerActionsConfig', (siteRights & 16) && ((serverFeatures & 128) != 0));
            QV('MainMenuMyFiles', siteRights & 8);
            QV('LeftMenuMyFiles', siteRights & 8);
            if (((siteRights & 8) == 0) && (xxcurrentView == 5)) { setDialogMode(0); go(1); }
            if (currentNode != null) { gotoDevice(currentNode._id, xxcurrentView, true); }

            // Update user image
            if ((userinfo.flags != null) && (userinfo.flags & 1)) {
                if (userinfo.accountImageRnd == null) { userinfo.accountImageRnd = Math.floor(Math.random() * 9999999999); }
                Q('p2AccountImage').src = 'userimage.ashx?rnd=' + userinfo.accountImageRnd;
            } else {
                Q('p2AccountImage').src = 'images/user-256.png';
            }

            // Update user management state
            if ((userinfo.siteadmin & 2) != 0) {
                // We are user administrator
                if (users == null) { meshserver.send({ action: 'users' }); }
                if (wssessions == null) { meshserver.send({ action: 'wssessioncount' }); }
                mainUpdate(8192 + 16384);
            } else {
                // We are not user administrator
                users = null;
                wssessions = null;
                mainUpdate(16384);
                if (xxcurrentView == 4 || ((xxcurrentView >= 30) && (xxcurrentView < 40))) { setDialogMode(0); go(1); currentUser = null; }
            }
            meshserver.send({ action: 'events', limit: parseInt(p3limitdropdown.value) });
            QV('ServerConsole', (userinfo.siteadmin === 0xFFFFFFFF) && ((serverFeatures & 16) != 0));
            QV('ServerTrace', (userinfo.siteadmin === 0xFFFFFFFF) && ((serverFeatures & 32) != 0));
            if ((xxcurrentView == 115) && (userinfo.siteadmin != 0xFFFFFFFF)) { go(6); }
            if ((xxcurrentView == 6) && ((userinfo.siteadmin & 21) == 0)) { go(1); }

            // If we are site administrator, register to get server statistics
            if ((siteRights & 21) != 0) { meshserver.send({ action: 'serverstats', interval: 10000 }); }
            updateUserDropdown();
        }

        // To boost the speed of the web page when even floods occur, this method perform a delayed update on the web page.
        var updateNaggleTimer = null;
        var updateNaggleFlags = 0;
        function mainUpdate(flags) {
            updateNaggleFlags |= flags;
            if (updateNaggleTimer == null) {
                updateNaggleTimer = setTimeout(function () {
                    if (updateNaggleFlags & 512) { center(); }
                    if (updateNaggleFlags & 1) { onSearchInputChanged(); }
                    if (updateNaggleFlags & 2) { onSortSelectChange(false); }
                    if (updateNaggleFlags & 128) { updateMeshes(); }
                    if (updateNaggleFlags & 4) { updateDevices(); updateDeviceDetails(); if (xxcurrentView == 21) { p21updateMesh(); } }
                    if (updateNaggleFlags & 8) { drawNotifications(); }
                    if ((updateNaggleFlags & 16) && (features & 0x00008000)) { updateMapMarkers(); }
                    if (updateNaggleFlags & 32) { eventsUpdate(); }
                    if ((updateNaggleFlags & 64) && (features & 0x00008000)) { refreshMap(false, true); }
                    if (updateNaggleFlags & 256) { drawDeviceTimeline(); }
                    if (updateNaggleFlags & 1024) { deviceEventsUpdate(); }
                    if (updateNaggleFlags & 2048) { userEventsUpdate(); }
                    if (updateNaggleFlags & 4096) { p20updateMesh(); }
                    if (updateNaggleFlags & 8192) { updateUserGroups(); }
                    if (updateNaggleFlags & 16384) { updateUsers(); }
                    if (updateNaggleFlags & 32768) { updateRecordings(); }
                    if (updateNaggleFlags & 65536) { updateLoginTokens(); }
                    updateNaggleTimer = null;
                    updateNaggleFlags = 0;
                    gotoStartViewPage();
                }, 150);
            }
        }

        // Return the number of 2nd factor for this account
        function count2factoraAuths() {
            if (userinfo == null) return -1;
            var authFactorCount = 0;
            if (userinfo.otpsecret == 1) { authFactorCount++; } // Authenticator time factor
            if (userinfo.otpduo == 1) { authFactorCount++; } // Duo factor
            if (userinfo.otpdev == 1) { authFactorCount++; } // Push authentication factor
            if (userinfo.otphkeys > 0) { authFactorCount += userinfo.otphkeys; } // FIDO hardware factor
            if ((features & 0x00800000) && (userinfo.otpekey == 1)) { authFactorCount++; } // EMail factor
            if ((features & 0x02000000) && (features & 0x04000000) && (userinfo.phone != null)) { authFactorCount++; } // SMS factor
            if ((features2 & 0x02000000) && (features2 & 0x04000000) && (userinfo.msghandle != null)) { authFactorCount++; } // Messaging factor
            if ((authFactorCount > 0) && (userinfo.otpkeys > 0) && ((features2 & 0x40000) == 0)) { authFactorCount++; } // Backup keys
            return authFactorCount;
        }

        var backupCodesWarningDone = false;
        function updateSelf() {
            if (!userinfo) return;
            var authFactorCount = count2factoraAuths(); // Get the number of 2nd factors
            var accountSettingsLocked = ((userinfo.siteadmin != 0xFFFFFFFF) && ((userinfo.siteadmin & 1024) != 0));
            QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
            QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true) && (accountSettingsLocked == false));
            QV('manageOtp', (serverinfo.lock2factor != true) && (authFactorCount > 0) && ((features2 & 0x40000) == 0));
            QV('authPhoneNumberCheck', (userinfo.phone != null));
            QV('authMessagingCheck', (userinfo.msghandle != null));
            QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
            QV('authDuoSetupCheck', (userinfo.otpduo == 1) && ((features2 & 0x20000000) != 0));
            QV('authAppSetupCheck', userinfo.otpsecret == 1);
            QV('manageAuthApp', (serverinfo.lock2factor != true) && ((userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0)));
            QV('manageDuoApp', (serverinfo.lock2factor != true) && ((features2 & 0x20000000) != 0));
            QV('authKeySetupCheck', userinfo.otphkeys > 0);
            QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 0x40) != 0));
            QV('authCodesSetupCheck', userinfo.otpkeys > 0);
            QV('managePushAuthDev', (serverinfo.lock2factor != true) && (features2 & 0x40) && (authFactorCount > 0));
            QV('manageHardwareOtp', (serverinfo.lock2factor != true));
            mainUpdate(4 + 128 + 4096);

            // Check if none or at least 2 factors are enabled.
            if ((backupCodesWarningDone == false) && (authFactorCount == 1) && ((features2 & 0x80000) == 0)) {
                addNotification({ text: "Agregar códigos de respaldo de dos factores. Si se pierde el factor actual, no hay forma de recuperar esta cuenta.", title: "Autenticación de dos factores", tag: 'backupcodes' });
                backupCodesWarningDone = true;
            }

            // If we can't create new groups, hide all links that can do that.
            var newGroupsAllowed = ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 64) == 0));
            QV('p2createMeshLink1', newGroupsAllowed);
            QV('p2createMeshLink2', newGroupsAllowed);
            QV('getStarted1', newGroupsAllowed);
            QV('getStarted2', !newGroupsAllowed);

            if (typeof userinfo.passchange == 'number') {
                if (userinfo.passchange == -1) { QH('p2nextPasswordUpdateTime', " - Reiniciar en el proximo inicio de sesión."); }
                else if ((passRequirements != null) && (typeof passRequirements.reset == 'number')) {
                    var seconds = (userinfo.passchange) + (passRequirements.reset * 86400) - Math.floor(Date.now() / 1000);
                    if (seconds < 0) { QH('p2nextPasswordUpdateTime', " - Reiniciar en el proximo inicio de sesión."); }
                    else if (seconds < 3600) { var secs = Math.floor(seconds / 60); QH('p2nextPasswordUpdateTime', format((secs == 1) ? " - Reiniciar en 1 minuto." : " - Reiniciar en {0} minutos.", secs)); }
                    else if (seconds < 86400) { var hours = Math.floor(seconds / 3600); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Reiniciar en 1 hora." : " - Reiniciar en {0} horas.", hours)); }
                    else { var days = Math.floor(seconds / 86400); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Reiniciar en 1 dia." : " - Reiniciar en {0} dias.", days)); }
                }
            }

            // Adjust "My Users" tabs
            QV('MainMenuMyUsers', ((users != null) && ((features & 4) == 0)) || (((userinfo.siteadmin & 512) != 0) && ((features & 0x08000000) != 0)));
            QV('LeftMenuMyUsers', ((users != null) && ((features & 4) == 0)) || (((userinfo.siteadmin & 512) != 0) && ((features & 0x08000000) != 0)));
            QV('UsersGeneral', ((users != null) && ((features & 4) == 0)));
            QV('UsersGroups', ((users != null) && ((features & 4) == 0)));
            QV('UsersRecordings', ((userinfo.siteadmin & 512) != 0) && ((features & 0x08000000) != 0));
        }

        function setSessionActivity() { sessionActivity = Date.now(); QH('idleTimeoutNotify', ''); }
        function checkIdleSessionTimeout() {
            var delta = (Date.now() - sessionActivity);
            if (delta > serverinfo.timeout) {
                if (desktop != null) { // Disconnect remote desktop
                    desktop.Stop();
                    webRtcDesktopReset();
                    desktopNode = desktop = null;
                    if (pluginHandler != null) { pluginHandler.callHook('onDesktopDisconnect'); }
                }
                if (terminal != null) { // Disconnect terminal
                    terminal.Stop();
                    terminal = null;
                }
                if (files != null) { // Disconnect files
                    files.Stop();
                    files = null;
                }
                if (serverinfo.logoutonidlesessiontimeout) {
                    if (urlargs.key) { window.location.href = 'logout?key=' + urlargs.key; }
					else { window.location.href = 'logout'; }
                }
            } else {
                var ds = Math.round((serverinfo.timeout - delta) / 1000);
                var sessionInProgress = (desktop != null || terminal != null || files != null);
                var show = serverinfo.logoutonidlesessiontimeout || sessionInProgress;
                var isLogout = serverinfo.logoutonidlesessiontimeout;
                var theText = '';
                if (ds <= 60) {
                    theText = isLogout
                        ? (ds == 1 ? "1 segundo hasta el cierre de sesión" : "{0} segundos hasta el inicio de sesión")
                        : (ds == 1 ? "1 segundo hasta desconectar" : "{0} segundos hasta desconectar");
                } else {
                    ds = Math.round(ds / 60);
                    if (ds <= 5) {
                        theText = isLogout
                            ? (ds == 1 ? "1 minuto hasta el cierre de sesión" : "{0} minutos hasta el cierre de sesión")
                            : (ds == 1 ? "1 minuto hasta desconectar" : "{0} minutos hasta desconectar");
                    }
                }
                QH('idleTimeoutNotify', show && theText ? '<br />' + format(theText, ds) : '');
            }
        }

        function onMessage(server, message) {
            switch (message.action) {
                case 'trace': {
                    serverTrace.unshift(message);
                    displayServerTrace();
                    break;
                }
                case 'intersession': {
                    if (message.subaction == 'removeNotify') { notificationDelete(message.id); }
                    break;
                }
                case 'traceinfo': {
                    if (typeof message.traceSources == 'object') {
                        if ((message.traceSources != null) && (message.traceSources.length > 0)) {
                            serverTraceSources = message.traceSources;
                            QH('p41traceStatus', EscapeHtml(message.traceSources.join(', ')));
                        } else {
                            serverTraceSources = [];
                            QH('p41traceStatus', "Ninguno");
                        }
                    }
                    break;
                }
                case 'serverstats': {
                    updateGeneralServerStats(message);
                    break;
                }
                case 'serverwarnings': {
                    if ((message.warnings != null) && (message.warnings.length > 0)) {
                        var ServerWarnings = {
                            1: "",
                            2: "Faltan parámetros WebDAV.",
                            3: "Opción de configuración desconocida \"{0}\".",
                            4: "Desactivada la compresión WebSocket, esta característica no funciona en NodeJS v11.11 a v12.15 y v13.2",
                            5: "No se puede cargar el certificado raíz TLS Intel AMT para el dominio por defecto.",
                            6: "No se puede cargar el certificado raíz TLS Intel AMT para el dominio {0}.",
                            7: "Los FQDN locales de CIRA no se usan cuando el servidor está en modo LAN-only o WAN-only.",
                            8: "No puede haber más de 4 FQDN locales de CIRA. Valor ignorado.",
                            9: "Se ha omitido la comprobación hash del agente, esto es inseguro.",
                            10: "Falta la dirección de correo de Let's Encrypt.",
                            11: "Nombres de host Let's Encrypt no válido.",
                            12: "Nombres Let's Encrypt no válidos, no pueden contener un *.",
                            13: "No se puede configurar el módulo Let's Encrypt.",
                            14: "Nombres Let's Encrypt no válidos, no se puede resolver: {0}",
                            15: "Dirección de correo Let's Encrypt no válida, no se puede resolver: {0}",
                            16: "No se puede cargar la lista de direcciones IPv6 de proxy de confianza Cloudfare.",
                            17: "El servidor SendGrid tiene un uso limitado en modo LAN.",
                            18: "El servidor SMTP tiene un uso limitado en modo LAN.",
                            19: "La puerta de enlace SMS tiene un uso limitado en modo LAN.",
                            20: "\"LoginCookieEncryptionKey\" no válido en config.json.",
                            21: "La ruta de la copia de seguridad no se puede configurar dentro de la carpeta meshcentral-data, la configuración de la copia de seguridad se ignora.",
                            22: "No se pudo firmar el agente {0}: {1}",
                            23: "No se puede cargar el archivo del icono del agente: {0}.",
                            24: "No se puede cargar el archivo del Logo del agente: {0}.",
                            25: "Esta versión de NodeJS no es compatible con OpenID.",
                            26: "Esta versión de NodeJS no es compatible con Discord.js.",
                            27: "Firebase now requires a service account JSON file, Firebase disabled."
                        };
                        var x = '';
                        for (var i in message.warnings) {
                            var y = message.warnings[i];
                            if (typeof y == 'string') {
                                x += '<div style=color:red;padding-bottom:6px><b>' + "ADVERTENCIA: " + y + '</b></div>';
                            } else {
                                var z = ServerWarnings[y.id];
                                if (z == null) { z = y.msg; } else { if (y.args != null) { z = format(z, ...y.args); } }
                                x += '<div style=color:red;padding-bottom:6px><b>' + "ADVERTENCIA: " + z + '</b></div>';
                            }
                        }
                        QH('serverWarnings', x);
                        QV('serverWarningsDiv', true);
                    }
                    break;
                }
                case 'servertimelinestats': {
                    setServerTimelineStats(message.events);
                    break;
                }
                case 'authcookie': {
                    // Got an authentication cookie refresh
                    authCookie = message.cookie;
                    authRelayCookie = message.rcookie;
                    break;
                }
                case 'serverinfo': {
                    serverinfo = message.serverinfo;
                    if (serverinfo.timeout) { setInterval(checkIdleSessionTimeout, 10000); checkIdleSessionTimeout(); }
                    if (debugmode == 1) { console.log('Server time: ', printDateTime(new Date(serverinfo.serverTime))); }
                    setupServiceWorker();
                    if (serverinfo.certExpire != null) {
                        var days = Math.floor((serverinfo.certExpire - Date.now()) / 86400000);
                        if ((days >= 0) && (days < 20)) {
                            QH('serverCertWarnings', '<div style=color:red;padding-bottom:6px><b>' + "ADVERTENCIA: " + format("El certificado vence en {0} día (s)", days) + '</b></div>');
                            QV('serverWarningsDiv', true);
                            addNotification({ text: format("El certificado vence en {0} día (s)", days) });
                        }
                    }
                    if (serverinfo.preConfiguredRemoteInput) {
                        // Setup the pre-configured keyboard remote input strings
                        var x = '';
                        for (var i in serverinfo.preConfiguredRemoteInput) { x += '<div class="cmtext" onclick="cmdeskpreconfigtypeaction(' + i + ',event)">' + EscapeHtml(serverinfo.preConfiguredRemoteInput[i].name) + '</div>'; }
                        if (x != '') { x += '<hr />'; }
                        QH('deskPreConfigShortcutContextMenu1', x);
                    }
                    if (serverinfo.preConfiguredScripts) {
                        // Setup the pre-configured scripts
                        var x = '', y = '';
                        for (var i in serverinfo.preConfiguredScripts) {
                            var s = serverinfo.preConfiguredScripts[i];
                            var z = '<div class="cmtext" onclick="cmdeskpreconfigscriptaction(' + i + ',event)">' + EscapeHtml(serverinfo.preConfiguredScripts[i].name) + '</div>';
                            if (s.type != 3) { x += z; }
                            if (s.type >= 3) { y += z; }
                        }
                        QH('deskPreConfigScriptContextMenu1', x); // Windows devices
                        QH('deskPreConfigScriptContextMenu2', y); // Other devices
                    }
                    break;
                }
                case 'userinfo': {
                    userinfo = message.userinfo;
                    updateSiteAdmin();
                    updateSelf();
                    updateUserDropdown();
                    break;
                }
                case 'users': {
                    users = {};
                    for (var m in message.users) { users[message.users[m]._id] = message.users[m]; }
                    if (currentUser != null) { currentUser = users[currentUser._id]; }
                    mainUpdate(16384);
                    updateSelf();
                    break;
                }
                case 'wssessioncount': {
                    wssessions = message.wssessions;
                    mainUpdate(16384);
                    break;
                }
                case 'meshes': {
                    meshes = {};
                    for (var m in message.meshes) { meshes[message.meshes[m]._id] = message.meshes[m]; }
                    if (currentMesh != null) { currentMesh = meshes[currentMesh._id]; }
                    mainUpdate(4 + 128);
                    break;
                }
                case 'usergroups': {
                    var groupCount = 0;
                    if (Array.isArray(message.ugroups)) {
                        usergroups = {};
                        for (var i in message.ugroups) { groupCount++; usergroups[message.ugroups[i]._id] = message.ugroups[i]; }
                        if (groupCount == 0) { usergroups = null; }
                    } else {
                        usergroups = message.ugroups;
                        for (var i in message.ugroups) { groupCount++; message.ugroups[i]._id = i; }
                        if (groupCount == 0) { usergroups = null; }
                    }
                    mainUpdate(8192);
                    break;
                }
                case 'files': {
                    filetree = setupBackPointers(message.filetree);
                    updateFiles();
                    d3updatefiles();
                    break;
                }
                case 'nodes': {
                    nodes = [];
                    for (var m in message.nodes) {
                        for (var n in message.nodes[m]) {
                            if (message.nodes[m][n]._id == null) { console.log('Invalid node (' + n + '): ' + JSON.stringify(message.nodes)); continue; }
                            if (message.nodes[m][n].name == null) { message.nodes[m][n].name = '???'; }
                            message.nodes[m][n].namel = message.nodes[m][n].name.toLowerCase();
                            if (message.nodes[m][n].rname) { message.nodes[m][n].rnamel = message.nodes[m][n].rname.toLowerCase(); } else { message.nodes[m][n].rnamel = message.nodes[m][n].namel; }
                            message.nodes[m][n].meshnamel = meshes[m] ? meshes[m].name.toLowerCase() : '*';
                            message.nodes[m][n].meshid = m;
                            message.nodes[m][n].state = (message.nodes[m][n].state) ? (message.nodes[m][n].state) : 0;
                            if (!message.nodes[m][n].icon) message.nodes[m][n].icon = 1;
                            message.nodes[m][n].ident = ++nodeShortIdent;
                            nodes.push(message.nodes[m][n]);
                        }
                    }

                    // Remove any stars for nodes that don't exist
                    for (var i in stars) { if (getNodeFromId(i) == null) { delete stars[i]; } }

                    // If we are currently looking at a node this is now gone, change the view.
                    if ((currentNode != null) && (IsNodeViewable(currentNode) == false)) { currentNode = null; go(1); }

                    // Change the reference to the current node
                    if (currentNode != null) { currentNode = getNodeFromId(currentNode._id); if (currentNode != null) { gotoDevice(currentNode._id, xxcurrentView, true); } else { go(1); } }

                    // Update device paging
                    devicePagingState = (message.totalcount == null) ? null : { total: message.totalcount, skip: message.skip, limit: message.limit };
                    updateDevicePageState();

                    mainUpdate(1 | 2 | 4 | 64);

                    // If groups are to be collapsed by default, do it now
                    if (collapseGroups === 'true' && Object.keys(CollapsedGroups).length === 0 && typeof(getstore('_collapse')) === 'undefined') { cmexpandaction(2); }

                    break;
                }
                case 'powertimeline': {
                    if (message.nodeid != powerTimelineReq) break;
                    powerTimelineNode = message.nodeid;
                    powerTimeline = message.timeline;
                    powerTimelineUpdate = Date.now() + 300000; // Update every 5 minutes
                    for (var i in powerTimeline) { if (i % 2 == 1) { powerTimeline[i] = powerTimeline[i] * 1000; } } // Decompress time
                    if (currentNode._id == message.nodeid) { mainUpdate(256); }
                    break;
                }
                case 'deviceShares': {
                    if (message.nodeid != deviceSharesReq) break;
                    deviceSharesNode = message.nodeid;
                    deviceShares = message.deviceShares;
                    if ((currentNode != null) && (currentNode._id == message.nodeid)) { gotoDevice(currentNode._id, xxcurrentView, true); }
                    break;
                }
                case 'deviceMeshShares': {
                    if (message.meshid != deviceSharesReq) break;
                    deviceSharesNode = message.meshid;
                    deviceShares = message.deviceShares;
                    if ((currentMesh != null) && (currentMesh._id == message.meshid)) { p20updateMesh(); }
                    break;
                }
                case 'getsysinfo': {
                    if (message.nodeid != powerTimelineReq) break;
                    if (message.noinfo === true) {
                        updateDeviceDetails(getNodeFromId(message.nodeid));
                    } else {
                        message.hardware.time = message.time;
                        updateDeviceDetails(getNodeFromId(message.nodeid), message.hardware);
                    }
                    break;
                }
                case 'software': {
                    try {
                        var softwareData = message.value;
                        if (JSON.parse(message.value).error) {
                            QH('p18Status', '<span>' + "ERROR: " + JSON.parse(message.value).error + '</span>');
                            QH('p18html', '<div style="text-align:center;padding:50px;color:gray"><i>' + "Haga clic en Actualizar para cargar el software instalado..." + '</i></div>');
                            installedAppsLoading = false;
                            installedAppsLoadingType = 0;
                            return;
                        }
                        if (typeof softwareData === 'string' && softwareData.trim().charAt(0) === '[' && softwareData.indexOf('"name"') >= 0) {
                            handleInstalledAppsResponse(softwareData);
                        } else if (typeof softwareData === 'string' && (softwareData.indexOf('"success"') >= 0 || softwareData.indexOf('"status"') >= 0 || softwareData.indexOf('"error"') >= 0)) {
                            handleUninstallResponse(softwareData);
                        } else {
                            installedAppsLoading = false;
                            installedAppsLoadingType = 0;
                            displayInstalledApps();
                        }
                    } catch (xx) { }
                    break;
                }
                case 'lastconnect': {
                    var node = getNodeFromId(message.nodeid);
                    if (node != null) {
                        node.lastconnect = message.time;
                        node.lastaddr = message.addr;
                        if ((currentNode._id == node._id) && (Q('MainComputerState').innerHTML == '') && (node.lastconnect != null)) {
                            QH('MainComputerState', '<span>' + "Visto por última vez:" + '<br />' + printDateTime(new Date(node.lastconnect)) + '</span>');
                        }
                    }
                    break;
                }
                case 'lastconnects': {
                    var lcnodes = Object.keys(message.lastconnects);
                    for (var i in lcnodes) {
                        var lcnodeid = lcnodes[i];
                        var node = getNodeFromId(lcnodeid);
                        if (node != null) { node.lastconnect = message.lastconnects[lcnodeid] }
                    }
                    mainUpdate(4);
					break;
                }
                case 'msg': {
                    // Check if this is a message from a node
                    if (message.nodeid != null) {
                        var index = -1;
                        if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == message.nodeid) { index = i; break; } } }
                        if (index != -1) {
                            // Node was found, dispatch the message
                            if ((message.type === 'cpuinfo') && (currentNode != null) && (currentNode._id == message.nodeid)) {
                                var now = (Date.now() / 1000), cpu = 0, memory = 0;
                                if (typeof message.cpu.total == 'number') { cpu = message.cpu.total; }
                                if (typeof message.memory.percentConsumed == 'number') { memory = message.memory.percentConsumed; }
                                deviceDetailsStatsData.push([now, cpu, memory]);
                                deviceDetailsStatsDraw(message);
							} else if (message.type === 'console') { p15consoleReceive(nodes[index], message.value, message.source); } // This is a console message.
                            else if (message.type === 'notify') { // This is a notification message.
                                var n = getstore('notifications', 0);
                                if (((n & 8) == 0) && (message.amtMessage != null)) { break; } // Intel AMT desktop & terminal messages should be ignored.
                                var n = { text: message.value, title: message.title, icon: message.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
                                if (message.id != null) { n.id = message.id; }
                                if (message.nodeid != null) { n.nodeid = message.nodeid; }
                                if (message.tag != null) { n.tag = message.tag; }
                                if (message.url != null) { n.url = message.url; }
                                if (message.username != null) { n.username = message.username; }
                                if (typeof message.maxtime === 'number') { n.maxtime = message.maxtime; }
                                addNotification(n);
                            } else if (message.type === 'ps') {
                                showDeskToolsProcesses(message);
                            } else if (message.type === 'services') {
                                showDeskToolsServices(message);
                            } else if (message.type === 'service') {
                                showServiceDetailsDialog(message);
                            } else if ((message.type === 'getclip') && (currentNode != null) && (currentNode._id == message.nodeid)) {
                                if ((message.tag == 1) && (xxdialogTag === 'clipboard')) {
                                    Q('d2clipText').value = message.data; // Put remote clipboard data into dialog box
                                } else if ((message.tag === 2) || (message.tag === 3)) {
                                    // tag 2 = manual clipboard-in button, tag 3 = auto-clipboard poll
                                    if ((navigator.clipboard != null) && (message.data != deskLastClipboardReceived) && document.hasFocus()) {
                                        deskLastClipboardReceived = message.data;
                                        deskLastClipboardSent = message.data; // prevent echo back to remote
                                        navigator.clipboard.writeText(message.data).then(function () { }).catch(function (err) { });
                                    }
                                }
                            } else if ((message.type === 'setclip') && (xxdialogTag === 'clipboard') && (currentNode != null) && (currentNode._id == message.nodeid)) {
                                // Display success/fail on the clipboard dialog box.
                                QH('dlgClipStatus', message.success ? '<span style=color:green>' + "Exitoso" + '</span>' : '<span style=color:red>' + "Falló" + '</span>')
                                setTimeout(function () { try { QH('dlgClipStatus', ''); } catch (ex) { } }, 2000);
                            } else if ((message.type === 'userSessions') && (currentNode != null) && (currentNode._id === message.nodeid) && (desktop == null)) {
                                // Got list of user sessions
                                var userSessions = [];
                                if (message.data != null) { for (var i in message.data) { if ((message.data[i].State == 'Active') || (message.data[i].State == 'Connected') || (message.data[i].StationName == 'Console') || (debugmode == 3)) { userSessions.push(message.data[i]); } } }
                                if (userSessions.length === 0) { connectDesktop(null, 1, null, message.tag); } // No active sessions, do a normal connection.
                                else if (userSessions.length === 1) { connectDesktop(null, 1, userSessions[0].SessionId, message.tag); } // One active session, connect to it
                                else {
                                    var x = '';
                                    var sortBy = "{{{userSessionsSort}}}";
                                    if (sortBy != '') {
                                        userSessions.sort(function (a, b) {
                                            if (!a[sortBy]) return -1; // a comes before b
                                            if (!b[sortBy]) return 1;  // b comes before a
                                            if (a[sortBy] < b[sortBy]) return -1;
                                            if (a[sortBy] > b[sortBy]) return 1;
                                            return 0;
                                        });
                                    }
                                    for (var i in userSessions) {
                                        x += '<div style="text-align:left;cursor:pointer;background-color:gray;margin:5px;padding:5px;border-radius:5px" onclick=connectDesktop(event,1,' + userSessions[i].SessionId + ',' + message.tag + ')>' + userSessions[i].State + (userSessions[i].StationName ? (', ' + userSessions[i].StationName) : '');
                                        if (userSessions[i].Username) { if (userSessions[i].Domain) { x += ' - ' + userSessions[i].Domain + '/' + userSessions[i].Username; } else { x += ' - ' + userSessions[i].Username; } }
                                        x += '</div>';
                                    }
                                    QH('p11DeskSessionSelector', x);
                                    QV('p11DeskSessionSelector', true);
                                }
                            } else if (message.type === 'psinfo') {
                                if (xxdialogTag === ('ps|' + message.nodeid + '|' + message.pid)) {
                                    var x = '<div style=max-height:200px;overflow-y:auto>';
                                    //x += addHtmlValue4("Process ID", message.pid);
                                    if ((typeof message.value == 'object') && (Object.keys(message.value).length > 0)) {
                                        // lets fix agent psinfo not being camelcase
                                        message.value = jsonToCamel(message.value);
                                        if (!message.value.cmd && message.value.path) { message.value.cmd = message.value.path }
                                        if (!message.value.processUser && message.value.userName) { message.value.processUser = message.value.userName.split("\\")[1]; }
                                        if (!message.value.processDomain && message.value.userName) { message.value.processDomain = message.value.userName.split("\\")[0]; }
                                        if (message.value.processName) { x += '<div style=padding:4px><div style=padding-bottom:4px>' + "Nombre del Proceso" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.processName + '</b></div></div>'; }
                                        if (message.value.machineName) { x += addHtmlValue5("Nombre de la Máquina", message.value.machineName); }
                                        if (message.value.cmd) { x += '<div style=padding:4px><div style=padding-bottom:4px>' + "Línea de Comandos" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.cmd + '</b></div></div>'; }
                                        if (message.value.mainWindowTitle) { x += '<div style=padding:4px><div style=padding-bottom:4px>' + "Título de Windows" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.mainWindowTitle + '</b></div></div>'; }
                                        if (message.value.processUser) { x += addHtmlValue5("Usuario", message.value.processUser); }
                                        if (message.value.processDomain) { x += addHtmlValue5("Dominio", message.value.processDomain); }
                                        if (message.value.startTime) { x += addHtmlValue5("Tiempo de Inicio", message.value.startTime); }
                                        x += '<hr />';
                                        if (message.value.PriorityBoostEnabled) { x += addHtmlValue5("Aumento de Prioridad", message.value.PriorityBoostEnabled ? "Habilitado" : "Deshabilitado"); }
                                        if (message.value.sessionId) { x += addHtmlValue5("ID de la Sesión", message.value.sessionId); }
                                        if (message.value.handleCount) { x += addHtmlValue5("Número de Manejadores", message.value.handleCount); }
                                        if (message.value.privilegedProcessorTime) { x += addHtmlValue5("Tiempo de Procesador Privilegiado", format("{0} segundos", message.value.privilegedProcessorTime)); }
                                        if (message.value.totalProcessorTime) { x += addHtmlValue5("Tiempo Total del Procesador", format("{0} segundos", message.value.totalProcessorTime)); }
                                        if (message.value.userProcessorTime) { x += addHtmlValue5("Tiempo de Procesador de Usuario", format("{0} segundos", message.value.userProcessorTime)); }
                                        if (message.value.nonpagedSystemMemorySize) { x += addHtmlValue5("Memoria No Paginada", getNiceSize2(message.value.nonpagedSystemMemorySize)); }
                                        if (message.value.pagedMemorySize) { x += addHtmlValue5("Memoria Paginada", getNiceSize2(message.value.pagedMemorySize)); }
                                        if (message.value.privateMemorySize) { x += addHtmlValue5("Memoria Privada", getNiceSize2(message.value.privateMemorySize)); }
                                        if (message.value.virtualMemorySize) { x += addHtmlValue5("Memoria Virtual", getNiceSize2(message.value.virtualMemorySize)); }
                                        if (message.value.workingSet) { x += addHtmlValue5("Conjunto de Trabajo", getNiceSize2(message.value.workingSet)); }
                                        if (message.value.peakWorkingSet) { x += addHtmlValue5("Conjunto de Trabajo Máximo", getNiceSize2(message.value.peakWorkingSet)); }
                                        if (message.value.peakPagedMemorySize) { x += addHtmlValue5("Memoria Paginada Máxima", getNiceSize2(message.value.peakPagedMemorySize)); }
                                        if (message.value.peakVirtualMemorySize) { x += addHtmlValue5("Memoria Virtual Máxima", getNiceSize2(message.value.peakVirtualMemorySize)); }
                                    } else {
                                        x += "No se proporcionó información";
                                    }
                                    x += '</div>';
                                    QH('id_dialogOptions', x);
                                }
                            } else if (message.type === 'deskBackground') {
                                if (message.data != "") {
                                    Q('DeskBackgroundButtonImage').style.color = '';
                                } else {
                                    Q('DeskBackgroundButtonImage').style.color = 'red';
                                }
                            }
                        }
                    } else {
                        if (message.type === 'notify') { // This is a notification message.
                            var n = { text: message.value, title: message.title, icon: message.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
                            if (message.id != null) { n.id = message.id; }
                            if (message.tag != null) { n.tag = message.tag; }
                            if (message.url != null) { n.url = message.url; }
                            if (message.username != null) { n.username = message.username; }
                            if (typeof message.maxtime == 'number') { n.maxtime = message.maxtime; }
                            addNotification(n);
                        }
                    }
                    break;
                }
                case 'getnetworkinfo': {
                    if (currentNode._id != message.nodeid) return;
                    updateDeviceDetails(getNodeFromId(message.nodeid), null, message);
                    if ((xxdialogMode == 2) && (xxdialogTag == 'if' + message.nodeid)) {
                        var x = '<div class=dialogText>';
                        if (currentNode.firstconnect) { x += addHtmlValue2("Conexión del primer agente", printDateTime(new Date(currentNode.firstconnect))); }
                        if (currentNode.lastconnect) { x += addHtmlValue2("Última conexión del agente", printDateTime(new Date(currentNode.lastconnect))); }
                        if (currentNode.lastaddr) {
                            var splitip = currentNode.lastaddr.split(':');
                            if (splitip.length > 2) {
                                // IPv6
                                x += addHtmlValue2("Última dirección IP del agente", currentNode.lastaddr + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(currentNode.lastaddr) + '")></i>');
                            } else {
                                // IPv4
                                if (isPrivateIP(currentNode.lastaddr)) {
                                    x += addHtmlValue2("Última dirección IP del agente", splitip[0] + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                } else {
                                    x += addHtmlValue2("Última dirección IP del agente", '<a href="https://iplocation.com/?ip=' + splitip[0] + '" rel="noreferrer noopener" target="MeshIPLoopup">' + splitip[0] + '</a> <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                }
                            }
                        }
                        if (message.updateTime != null) { x += addHtmlValue2("Última actualización de interfaces", printDateTime(new Date(message.updateTime))); }

                        if (message.netif != null) {
                            // Old style
                            for (var i in message.netif) {
                                var net = message.netif[i];
                                x += '<hr />'
                                if (net.name) { x += addHtmlValue2("Nombre", '<b>' + EscapeHtml(net.name) + '</b>'); }
                                if (net.desc) { x += addHtmlValue2("Descripción", EscapeHtml(net.desc).replace('(R)', '&reg;').replace('(r)', '&reg;')); }
                                if (net.dnssuffix) { x += addHtmlValue2("Sufijo DNS", EscapeHtml(net.dnssuffix) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar nombre al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.dnssuffix) + '")></i>'); }
                                if (net.mac) { x += addHtmlValue2("Dirección MAC", '<a href="https://maclookup.app/search/result?mac=' + net.mac.substring(0, 6) + '" rel="noreferrer noopener" target="MeshMACLoopup">' + EscapeHtml(net.mac.toLowerCase()) + '</a> <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar Direccion MAC al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.mac.toLowerCase()) + '")></i>'); }
                                if (net.v4addr) { x += addHtmlValue2("Dirección IPv4", EscapeHtml(net.v4addr) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4addr) + '")></i>'); }
                                if (net.v4mask) { x += addHtmlValue2("Máscara IPv4", EscapeHtml(net.v4mask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4mask) + '")></i>'); }
                                if (net.v4gateway) { x += addHtmlValue2("Puerta de Enlace IPv4", EscapeHtml(net.v4gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4gateway) + '")></i>'); }
                                if (net.gatewaymac) { x += addHtmlValue2("MAC de la Puerta de Enlace", '<a href="https://maclookup.app/search/result?mac=' + net.gatewaymac.substring(0, 6) + '" rel="noreferrer noopener" target="MeshMACLoopup">' + EscapeHtml(net.gatewaymac.toLowerCase()) + '</a> <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar Direccion MAC al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.gatewaymac.toLowerCase()) + '")></i>'); }
                            }
                        } else if (message.netif2 != null) {
                            // New style
                            for (var i in message.netif2) {
                                var net = message.netif2[i];
                                if ((Array.isArray(net) == false) || (net.length < 1) || (net[0] == null) || ((typeof net[0].mac == 'string') && (net[0].mac.startsWith('00:00:00:00')))) continue;
                                x += '<hr />'
                                x += addHtmlValue2("Nombre", '<b>' + EscapeHtml(i) + '</b>');
                                if (typeof net[0].mac == 'string') { x += addHtmlValue2("Dirección MAC", '<a href="https://maclookup.app/search/result?mac=' + net[0].mac.split(':').join('').substring(0, 6) + '" rel="noreferrer noopener" target="MeshMACLoopup">' + EscapeHtml(net[0].mac.toLowerCase()) + '</a> <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar Direccion MAC al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net[0].mac.toLowerCase()) + '")></i>'); }
                                if (net[0].fqdn) { x += addHtmlValue2("FQDN", EscapeHtml(net[0].fqdn)); }
                                for (var j = 0; j < net.length; j++) {
                                    var netif = net[j];
                                    if (netif.family == 'IPv6') {
                                        if (netif.address) { x += addHtmlValue2("Dirección IPv6", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("Máscara IPv6", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("Puerta de Enlace IPv6", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    } else if (netif.family == 'IPv4') {
                                        if (netif.address) { x += addHtmlValue2("Dirección IPv4", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("Máscara IPv4", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("Puerta de Enlace IPv4", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copiar dirección al portapapeles" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    }
                                }
                            }
                        } else {
                            x += '<hr />'
                            x += "No hay información de interfaz de red disponible para este dispositivo.";
                        }
                        x += '</div>';
                        QH('d2netinfo', x);
                    }
                    break;
                }
                case 'backuprestore': {
                    if (message.dialog == 'backup') {
                        server_showBackupDlgUpdate(message.data);
                    } else {
                        server_showRestoreDlgUpdate(message.data);
                    }
                    break;
                }
                case 'serverversion': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'MeshCentralServerUpdate')) {
                        var x = '<div class=dialogText>';
                        if (!message.tags.current) { message.tags.current = "Desconocido"; }
                        if (!message.tags.stable) { message.tags.stable = "Desconocido"; }
                        if (!message.tags.latest) { message.tags.latest = "Desconocido"; }
                        x += addHtmlValue2("Versión Actual", '<b>' + EscapeHtml(message.tags.current) + '</b>');
                        x += '<hr />';
                        x += addHtmlValue2('<label><input id=d2updateCheck1 type=checkbox class="form-check-input me-2" onclick=server_showVersionDlgUpdate()' + (((message.tags.stable == "Desconocido") || (message.tags.current == message.tags.stable) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Versión Estable" + '</label>', '<b>' + EscapeHtml(message.tags.stable) + '</b>');
                        x += addHtmlValue2('<label><input id=d2updateCheck2 type=checkbox class="form-check-input me-2" onclick=server_showVersionDlgUpdate()' + (((message.tags.latest == "Desconocido") || (message.tags.current == message.tags.latest) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Última Versión" + '</label>', '<b>' + EscapeHtml(message.tags.latest) + '</b>');
                        x += '</div>';
                        if (((message.tags.current == message.tags.latest) && (message.tags.current == message.tags.stable)) || ((features & 2048) == 0)) {
                            setModalContent('xxAddAgent', "Versión de MeshCentral", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        } else {
                            x += '<hr />' + "Marca y haz clíc en Aceptar para iniciar la actualización automática del servidor.";
                            xxdialogTag = message.tags;
                            setModalContent('xxAddAgent', "Versión de MeshCentral", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                                server_showVersionDlgEx(2, message.tags);
                            });
                            server_showVersionDlgUpdate();
                        }
                    }
                    break;
                }
                case 'servererrors': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'MeshCentralServerErrors')) {
                        if (message.data == null) {
                            setModalContent('xxAddAgent', "Server Errors", "El servidor no tiene registro de errores.");
                            showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        } else {
                            var x = '<div class="dialogText dialogTextLog"><pre id=d2ServerErrorsLogPre>' + EscapeHtml(message.data) + '</pre></div>';
                            x += '<br /><div style=float:right><i role=button class="fa-fw fa-solid fa-download fa-sm" title="' + "Descargar registro de errores" + '" onclick=d2CopyServerErrorsToClip()></i></div>';
                            x += '<div><label><input id=d2clearErrorsCheck type=checkbox class="form-check-input me-2" onclick=server_showErrorsDlgUpdate() /> ' + "Marca y haz clic en Aceptar para borrar el registro de errores." + '</label></div>';
                            setModalContent('xxAddAgent', "Errores del Servidor MeshCentral", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', server_showErrorsDlgEx);
                            server_showErrorsDlgUpdate();
                        }
                    }
                    break;
                }
                case 'serverconfig': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'MeshCentralServerConfig')) {
                        if (message.data == null) {
                            setModalContent('xxAddAgent', "Server Configuration", 'Server has no config file.', 'extra-large');
                        } else {
                            setModalContent('xxAddAgent', "Server Configuration", 4, 'extra-large');
                            QV('d4EncodingButton', false);
                            QV('d4LineBreakButton', false);
                            Q('d4editorarea').value = message.data;
                            Q('d4editorarea').setAttribute('readonly', 'readonly');
                        }
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    }
                    break;
                }
                case 'serverconsole': {
                    p15consoleReceive('serverconsole', message.value);
                    break;
                }
                case 'events': {
                    if ((message.nodeid != null) && (currentNode != null) && (message.nodeid == currentNode._id)) {
                        currentDeviceEvents = message.events;
                        mainUpdate(1024);
                    } else if ((message.userid != null) && (currentUser != null) && (message.userid == currentUser._id)) {
                        currentUserEvents = message.events;
                        mainUpdate(2048);
                    } else {
                        events = message.events;
                        mainUpdate(32);
                    }
                    break;
                }
                case 'recordings': {
                    p52recordings = message.events;
                    if (message.error != null) { p52recordings = message.error; }
                    updateRecordings();
                    break;
                }
                case 'getcookie': {
                    if (message.tag == 'MCRouter') {
                        var servername = serverinfo.name;
                        if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
                        var domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1);
                        var portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port);
                        var url = 'mcrouter://' + servername + portStr + domainUrl + 'control.ashx?c=' + authCookie + '&t=' + serverinfo.tlshash + '&l={{{lang}}}' + (urlargs.key ? ('&key=' + urlargs.key) : '');
                        if (message.nodeid != null) { url += ('&nodeid=' + message.nodeid); }
                        if (message.tcpport != null) { url += ('&protocol=1&remoteport=' + message.tcpport); }
                        if (message.localRelay) { url += '&local=1'; }
                        if (message.localport) { url += '&localport=' + message.localport; }
                        if (message.name != null) { url += ('&name=' + encodeURIComponentEx(message.name)); }
                        if (message.ip != null) { url += ('&remoteip=' + message.ip); }
                        url += ('&appid=' + message.protocol + '&autoexit=1'); // Protocol: 0 = Custom, 1 = HTTP, 2 = HTTPS, 3 = RDP, 4 = PuTTY, 5 = WinSCP, 6 = MCRDesktop, 7 = MCRFiles
                        //console.log(url);
                        downloadFile(url, '');
                    } else if (message.tag == 'novnc') {
                        var vncurl = window.location.origin + domainUrl + 'novnc/vnc.html?ws=wss%3A%2F%2F' + window.location.host + encodeURIComponentEx(domainUrl) + (message.localRelay ? 'local' : 'mesh') + 'relay.ashx%3Fauth%3D' + message.cookie + '&show_dot=1' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '&l={{{lang}}}';
                        var node = getNodeFromId(message.nodeid);
                        if (node != null) { vncurl += '&name=' + encodeURIComponentEx(node.name); }
                        safeNewWindow(vncurl, 'mcnovnc/' + message.nodeid);
                    } else if (message.tag == 'mstsc') {
                        var rdpurl = window.location.origin + domainUrl + 'mstsc.html?ws=' + message.cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
                        var node = getNodeFromId(message.nodeid);
                        if (node != null) { rdpurl += '&name=' + encodeURIComponentEx(node.name); }
                        if (message.localRelay) { rdpurl += '&local=1'; }
                        safeNewWindow(rdpurl, 'mcmstsc/' + message.nodeid);
                    } else if (message.tag == 'ssh') {
                        var sshurl = window.location.origin + domainUrl + 'ssh.html?ws=' + message.cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
                        var node = getNodeFromId(message.nodeid);
                        if (node != null) { sshurl += '&name=' + encodeURIComponentEx(node.name); }
                        if (message.localRelay) { sshurl += '&local=1'; }
                        safeNewWindow(sshurl, 'mcssh/' + message.nodeid);
                    }
                    break;
                }
                case 'getNotes': {
                    var n = Q('d2devNotes');
                    if (n && (message.id == decodeURIComponent(n.attributes['noteid'].value))) {
                        if (message.notes) { QH('d2devNotes', decodeURIComponent(message.notes)); } else { QH('d2devNotes', ''); }
                        var ro = (n.attributes['ro'].value == 'true');
                        if (ro == false) { // If we have permissions, set read/write on this note.
                            n.removeAttribute('readonly');
                            QE('idx_dlgOkButton', true);
                            QV('idx_dlgOkButton', true);
                            focusTextBox('d2devNotes');
                        }
                    } else {
                        Q('notesPanelArea').innerHTML = (message.notes && marked && DOMPurify) ? DOMPurify.sanitize(marked.parse(decodeURIComponent(message.notes), { breaks: true }), { USE_PROFILES: { html: true } }) : '';
                        if ((showNotesPanel === 'true') && message.notes) { QV('notesPanel', true); } else { QV('notesPanel', false); }
                    }
                    break;
                }
                case 'otpauth-request': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-request')) {
                        if (message.err != null) {
                            var otpauthErrors = ['', "2FA está bloqueado", "Los códigos de respaldo están bloqueados", "Token de inicio de sesión en uso", "OTP 2FA no permitido", "La cuenta esta bloqueada", "No se puede cargar OTPLIB"];
                            if ((message.err > 0) && (message.err < otpauthErrors.length)) { QH('d2optinfo', otpauthErrors[message.err]); } else { QH('d2optinfo', format("Error n.º {0}", message.err)); }
                        } else {
                            var secret = message.secret;
                            if (secret.length == 52) { secret = secret.split(/(.............)/).filter(Boolean).join(' '); }
                            else if (secret.length == 32) { secret = secret.split(/(....)/).filter(Boolean).join(' '); secret = secret.substring(0, 20) + '<br/>' + secret.substring(20) }
                            QH('d2optinfo', '<table><tr><td style=vertical-align:top>' + format("Instalar" + ' <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" rel="noreferrer noopener" target=_blank>' + "Google Authenticator" + '</a> ' + "o una aplicación compatible y escanea el código de barras, usa <a href=\"{0}\" rel=\"noreferrer noopener\" target=_blank> este enlace </a> o ingresa el secreto. Luego, ingresa el token actual de 6 dígitos a continuación para activar el inicio de sesión en 2 pasos.", message.url) + '<br /><br />' + 'Secret <img src=images/link4.png height=10 width=10 title="' + "Copy Secret to clipboard" + '" style=cursor:pointer onclick=d2CopySecretToClip()>' + '<br /><tt id=d2optsecret secret="' + message.secret + '" style=font-size:12px>' + secret + '</tt><br /><br /></td><td style=width:1px;vertical-align:top><a href="' + message.url + '" rel="noreferrer noopener" target=_blank><div id="qrcode"></div></a></td><tr><td colspan=2 style="text-align:center;border-top:1px solid black"><br />' + "Ingresa el token aquí para iniciar sesión en 2 pasos:" + ' <input type=text autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=account_addOtpCheck(event) onkeydown=account_addOtpCheck() maxlength=6 id=d2otpauthinput type=text></td></table>');
                            new QRCode(Q('qrcode'), { text: message.url, width: 128, height: 128, colorDark: '#000000', colorLight: '#EEE', correctLevel: QRCode.CorrectLevel.H });
                            QV('idx_dlgOkButton', true);
                            QE('idx_dlgOkButton', false);
                            Q('d2otpauthinput').focus();
                        }
                    }
                    break;
                }
                case 'otpauth-setup': {
                    if (xxdialogMode) return;
                    setModalContent('xxAddAgent', "Aplicación de Autenticación", message.success ? ('<b style=color:green>' + "Activación de la Aplicación de Autenticación exitosa." + '</b> ' + "Ahora necesitarás un token válido para iniciar sesión nuevamente.") : ('<b style=color:red>' + "La activación de inicio de sesión en 2 pasos falló." + '</b> ' + "Borra el secreto de la aplicación e intenta nuevamente. Sólo tienes unos minutos para ingresar el código adecuado."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-clear': {
                    if (xxdialogMode) return;
                    setModalContent('xxAddAgent', "Aplicación de Autenticación", message.success ? ('<b>' + "Aplicación de Autenticación eliminada." + '</b> ' + "Puedes reactivar esta función en cualquier momento.") : ('<b style=color:red>' + "La eliminación de activación de inicio de sesión en 2 pasos falló." + '</b> ' + "Intentar nuevamente."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-getpasswords': {
                    if (xxdialogMode == 2 && (xxdialogTag != 'otpauth-manage')) return;
                    var x = "Los tokens únicos se pueden usar como autenticación secundaria. Genera un conjunto, imprímelos y guárdalos en un lugar seguro.";
                    x += '<div style="border-radius:6px;border: 2px dashed #888;width:100%;margin-top:8px"><div style="padding:8px;font-family:Arial, Helvetica, sans-serif;font-size:20px;font-weight:bold"><table class=selecttext style=width:100%;text-align:center>';
                    if (message.passwords) {
                        var j = 0, clipb = '';
                        for (var i in message.passwords) {
                            if (++j % 2) { x += '<tr>'; }
                            var p = '' + message.passwords[i].p;
                            while (p.length < 8) { p = '0' + p; }
                            if (message.passwords[i].u === true) {
                                x += '<td>' + p.substring(0, 4) + '&nbsp;' + p.substring(4);
                                if (clipb != '') { clipb += ' '; }
                                clipb += p;
                            } else {
                                x += '<td><strike style=color:#BBB>' + p.substring(0, 4) + '&nbsp;' + p.substring(4); + '</strike>';
                            }
                        }
                    } else {
                        x += '<tr><td>' + "No hay Tokens Activos";
                    }
                    x += '</table></div></div><br />';
                    x += '<div class="btn-group">';
                    x += '<button type=button class="btn btn-warning" onclick="account_manageOtp(1);">' + "Generar Nuevos Tokens" + '</input>';
                    if (message.passwords != null) {
                        x += '<button type=button class="btn btn-danger" onclick="account_manageOtp(2);">' + "Borrar Tokens" + '</button></div>';
                        x += '&nbsp;<i class="fa-regular fa-clipboard" title="' +  "Copiar códigos válidos al portapapeles" + '" style="cursor:pointer" onclick=copyTextToClip2("' + encodeURIComponentEx(clipb) + '")></i>';
                    } else {
                        x += '</div>'
                    }
                    xxdialogTag = 'otpauth-manage';
                    setModalContent('xxAddAgent', "Administrar Códigos de Respaldo", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otp-hkey-get': {
                    if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
                    var start = '<div style="border-radius:6px;border:2px solid #CCC;background-color:#BBB;width:100%;box-sizing:border-box;margin-bottom:6px"><div style="margin:3px;font-family:Arial, Helvetica, sans-serif;font-size:16px;font-weight:bold"><table style=width:100%;text-align:left>';
                    var end = '</table></div></div>';
                    var x = '<a href="https://www.yubico.com/" rel="noreferrer noopener" target="_blank">' + "Las claves de Hardware" + '</a> ' + "son usadas como segundo factor de autenticación.";
                    x += '<div class="backgroundContainer" style="max-height:150px;overflow-y:auto;overflow-x:hidden;margin-top:6px;margin-bottom:6px">';
                    if (message.keys && message.keys.length > 0) {
                        for (var i in message.keys) {
                            var key = message.keys[i], type = (key.type == 2) ? 'OTP' : 'WebAuthn';
                            x += start + '<tr style=margin:5px><td style=width:30px><img width=24 height=18 src="images/hardware-key-' + type + '-24.png" style=margin-top:4px><td style=width:250px>' + key.name + '<td style=text-align:right><button type=button class="btn btn-danger" onclick=account_removehkey(' + key.i + ')>' + "Remover" + '</button>' + end;
                        }
                    } else {
                        x += start + '<tr style=text-align:center><td>' + "No hay Claves Configuradas" + end;
                    }
                    x += '</div>';
                    x += '<div>';
                    var hkeycount = (typeof userinfo.otphkeys == 'number') ? userinfo.otphkeys : 0;
                    if ((typeof serverinfo.maxfidokeys != 'number') || (serverinfo.maxfidokeys > hkeycount)) { // Check if we we reached maximum hardware keys
                        if ((features & 0x00020000) != 0) { x += '<button class="btn btn-primary" id=d2addkey3 type=button onclick="account_addhkey(3);">' + "Agregar llave" + '</input>'; }
                        if ((features & 0x00004000) != 0) { x += '<button class="btn btn-primary" id=d2addkey2 type=button onclick="account_addhkey(2);">' + "Agregar YubiKey&reg; OTP" +'</input>'; }
                    } else {
                        x += "Número máximo de claves alcanzado.";
                    }
                    x += '</div><br />';
                    xxdialogTag = 'otpauth-hardware-manage';
                    setModalContent('xxAddAgent', "Administrar Claves de Seguridad", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    if (u2fSupported() == false) { QE('d2addkey1', false); }
                    break;
                }
                case 'otp-hkey-yubikey-add': {
                    if (message.result) {
                        meshserver.send({ action: 'otp-hkey-get' }); // Success, ask for the full list of keys.
                    } else {
                        setModalContent('xxAddAgent', "Agregar Llave de Seguridad", '<br />' + "Error, no se puede agregar la llave." + '<br /><br />');
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    }
                    break;
                }
                case 'otp-hkey-setup-response': {
                    if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
                    if (message.result == true) {
                        meshserver.send({ action: 'otp-hkey-get' }); // Success, ask for the full list of keys.
                    } else {
                        setModalContent('xxAddAgent', "Agregar Llave de Seguridad", '<br />' + "Error, no se puede agregar la llave." + '<br /><br />');
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');

                    }
                    break;
                }
                case 'webauthn-startregister': {
                    if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
                    var x = "Presione la tecla ahora." + '<br /><br /><div style=width:100%;text-align:center><img width=120 height=117 src="images/hardware-keypress-120.png" /></div><input id=dp1keyname style=display:none value=' + message.name + ' />';
                    setModalContent('xxAddAgent', "Agregar Llave de Seguridad", x);
                    var publicKey = message.request;
                    message.request.challenge = Uint8Array.from(atob(message.request.challenge), function (c) { return c.charCodeAt(0) })
                    message.request.user.id = Uint8Array.from(atob(message.request.user.id), function (c) { return c.charCodeAt(0) })
                    navigator.credentials.create({ publicKey: publicKey }).then(function (newCredentialInfo) {
                        // Public key credential
                        meshserver.send({ action: 'webauthn-endregister', response: { rawId: btoa(String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.rawId))), response: { attestationObject: btoa(String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.attestationObject))), clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON))) }, type: newCredentialInfo.type } });
                    }, function (error) {
                        // Error
                        console.log("ERROR: " + error);
                        setModalContent('xxAddAgent', "Agregar Llave de Seguridad", "ERROR: " + error);
                    });
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'verifyPhone': {
                    if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return;
                    var x = '<table><tr><td><img src="images/phone80.png" style=padding:8px>';
                    x += '<td>' + "Revisa tu teléfono e ingresa el código de verificación.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Código de verificación:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=6 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>';
                    setModalContent('xxAddAgent', "Notificaciones Telefónicas", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_managePhoneConfirm(3, message.cookie));
                    Q('d2phoneCodeInput').focus();
                    account_managePhoneCodeValidate();
                    break;
                }
                case 'verifyMessaging': {
                    if (xxdialogMode && (xxdialogTag != 'verifyMessaging')) return;
                    var x = '<table><tr><td><img src="images/messaging40.png" style=padding:8px>';
                    x += '<td>' + "Revisa tu aplicacion de mensajes e introduce el codigo de verificacion.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Código de verificación:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=6 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>';
                    setModalContent('xxAddAgent', "Notificaciones de Mensajería", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_manageMessagingConfirm(3, message.cookie));
                    Q('d2phoneCodeInput').focus();
                    account_managePhoneCodeValidate();
                    break;
                }
                case 'fileoperation': {
                    // View the file in the dialog box
                    var p5editSaveBack = function (b, tag) {
                        var data;
                        var value = Q('d4editorarea').value;
                        value = d4EditLineBreakVal === 0 ? value.replace(/\r?\n|\r/g, '\r\n') : // Windows
                            d4EditLineBreakVal === 2 ? value.replace(/\r\n|\n/g, '\r') : // Mac
                                value.replace(/\r\n|\r/g, '\n'); // Linux
                        if (d4EditEncodingVal == 1) {
                            data = encode_utf8(value); // UTF8 encoding
                        } else {
                            data = value; // RAW encoding
                        }
                        meshserver.send({ action: 'fileoperation', fileop: 'set', path: tag.path, file: tag.file, data: btoa(data) });
                    }
                    setModalContent('xxAddAgent', EscapeHtml(message.file), 4, 'extra-large');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p5editSaveBack(3, message));
                    QV('d4EncodingButton', true);
                    QV('d4LineBreakButton', true);
                    if (d4EditEncodingVal == 1) {
                        Q('d4editorarea').value = decode_utf8(atob(message.data)); // UTF8 Encoding
                    } else {
                        Q('d4editorarea').value = atob(message.data); // RAW Encoding
                    }
                    break;
                }
                case 'event': {
                    if (!message.event.nolog) {
                        if (currentNode && (message.event.nodeid == currentNode._id) && (currentDeviceEvents != null)) {
                            // If this event has a nodeid and we are looking at this node, update the log in real time.
                            if ((message.event.action == p16filterevents.value) || (p16filterevents.value == "")) {
                                if(currentDeviceEvents != null) {
                                    currentDeviceEvents.unshift(message.event);
                                    var eventLimit = parseInt(p16limitdropdown.value);
                                    while (currentDeviceEvents.length > eventLimit) { currentDeviceEvents.pop(); } // Remove element(s) at the end
                                }
                                mainUpdate(1024);
                            }
                        }

                        if (currentUser && (message.event.userid == currentUser._id)) {
                            // If this event has a userid and we are looking at this user, update the log in real time.
                            if ((message.event.action == p31filterevents.value) || (p31filterevents.value == "")) {
                                if(currentUserEvents != null) {
                                    currentUserEvents.unshift(message.event);
                                    var eventLimit = parseInt(p31limitdropdown.value);
                                    while (currentUserEvents.length > eventLimit) { currentUserEvents.pop(); } // Remove element(s) at the end
                                }
                                mainUpdate(2048);
                            }
                        }

                        // Add this event to the main events log.
                        if ((message.event.action == p3filterevents.value) || (p3filterevents.value == "")) {
                            if(events != null) {
                                events.unshift(message.event);
                                var eventLimit = parseInt(p3limitdropdown.value);
                                while (events.length > eventLimit) { events.pop(); } // Remove element(s) at the end
                            }
                            mainUpdate(32);
                        }
                    }
                    if (message.event.noact) break; // Take no action on this event

                    switch (message.event.action) {
                        case 'serverinfochange': {
                            if (message.event.lock2factor != null) { serverinfo.lock2factor = message.event.lock2factor; updateSelf(); updateSiteAdmin(); }
                            break;
                        }
                        case 'deviceShareUpdate': {
                            if (message.event.nodeid != deviceSharesReq) break;
                            deviceSharesNode = message.event.nodeid;
                            deviceShares = message.event.deviceShares;
                            if (currentNode._id == deviceSharesNode) { gotoDevice(currentNode._id, xxcurrentView, true); }
                            break;
                        }
                        case 'agentlog': {
                            if (message.event.msgid == 98) {
                                // This is a agent help request, popup a notification.
                                if (GetNodeRights(message.event.nodeid) > 0) {
                                    var n = getNodeFromId(message.event.nodeid);
                                    addNotification({ text: format("Ayuda solicitada a {0}: {1}", message.event.msgArgs[0], message.event.msgArgs[1]), title: n.name, icon: n.icon, nodeid: message.event.nodeid });
                                }
                            }
                            break;
                        }
                        case 'recording': {
                            if ((p52recordings != null) && (typeof p52recordings == 'object')) { p52recordings.unshift(message.event); message.event.present = 1; updateRecordings(); }
                            break;
                        }
                        case 'userWebState': {
                            // New user web state, update the web page as needed
                            try {
                                if (localStorage != null) {
                                    // TODO: The problem with this "old" values is that if changed from a different tab, the old and new values will be the same.
                                    // So comparing against these values is not a good idea.
                                    var oldShowRealNames = localStorage.getItem('showRealNames');
                                    var oldUiMode = localStorage.getItem('uiMode');
                                    var oldSort = localStorage.getItem('sort');
                                    var oldLoctag = localStorage.getItem('loctag');
                                    var oldNightMode = localStorage.getItem('nightMode');
                                    var oldFooterBar = localStorage.getItem('footerBar');

                                    var webstate = JSON.parse(message.event.state);
                                    for (var i in webstate) { localStorage.setItem(i, webstate[i]); }
                                    customIconValues = loadCustomIconState();
                                    applyIconCustomization(customIconValues);

                                    // Update the web page
                                    //if ((webstate.deskAspectRatio != null) && (webstate.deskAspectRatio != deskAspectRatio)) { deskAspectRatio = webstate.deskAspectRatio; deskAdjust(); }
                                    if ((webstate.showRealNames != null) && (webstate.showRealNames != oldShowRealNames)) { showRealNames = Q('RealNameCheckBox').checked = (webstate.showRealNames == '1'); mainUpdate(6); }
                                    if ((webstate.uiMode != null) && (webstate.uiMode != oldUiMode)) { userInterfaceSelectMenu(parseInt(webstate.uiMode)); }
                                    if ((webstate.sort != null) && (webstate.sort != oldSort)) { document.getElementById('sortselect').selectedIndex = sort = parseInt(webstate.sort); mainUpdate(6); }
                                    if ((webstate.loctag != null) && (webstate.loctag != oldLoctag)) { if (webstate.loctag != null) { args.locale = webstate.loctag; } else { delete args.locale; } mainUpdate(0xFFFFFFFF); }
                                    if ((webstate.nightMode != null) && (webstate.nightMode != oldNightMode)) { putstore('nightMode', webstate.nightMode); setNightMode(); }
                                    if ((webstate.footerBar != null) && (webstate.footerBar != oldFooterBar)) { footerBar = (webstate.footerBar == '1'); QS('container')['grid-template-rows'] = null; QS('container')['-ms-grid-rows'] = null; adjustPanels(); }
                                    if ((webstate.stars != null) && (webstate.stars != JSON.stringify(stars))) {
                                        stars = JSON.parse(webstate.stars);
                                        if (Q('DevFilterSelect').value == 3) { mainUpdate(5); } else { mainUpdate(4); }
                                        if (currentNode) { refreshDevice(currentNode._id); }
                                    }
                                    if (webstate.deskKeyShortcuts != null) {
                                        // Set the user's desktop shortcut keys
                                        deskKeyboardShortcuts = [];
                                        var deskKeyboardShortcutsStr = webstate.deskKeyShortcuts.split(',');
                                        for (var i in deskKeyboardShortcutsStr) { if (deskKeyboardShortcutsStr[i] != "") { deskKeyboardShortcuts.push(parseInt(deskKeyboardShortcutsStr[i])); } }
                                        updateDeskShortcutKeys();
                                    }
                                    if (webstate.deskStrings != null) {
                                        // Set the user's desktop strings
                                        var s = null;
                                        try { s = JSON.parse(webstate.deskStrings); } catch (ex) { }
                                        if (Array.isArray(s)) { deskKeyboardStrings = s; updateDesktopStrings(); }
                                    }
                                }
                            } catch (ex) { }
                            break;
                        }
                        case 'servertimelinestats': { addServerTimelineStats(message.event.data); break; }
                        case 'accountcreate':
                        case 'accountchange': {
                            // An account was created or changed
                            if ((typeof message.event.account != 'object') || (message.event.account == null)) { console.log(message.event); return; }
                            if (userinfo._id == message.event.account._id) {
                                var newsiteadmin = message.event.account.siteadmin ? message.event.account.siteadmin : 0;
                                var oldsiteadmin = userinfo.siteadmin ? userinfo.siteadmin : 0;
                                var newRemoveRights = message.event.account.removeRights ? message.event.account.removeRights : 0;
                                var oldRemoveRights = userinfo.removeRights ? userinfo.removeRights : 0;
                                if ((message.event.account.quota != userinfo.quota) || (((userinfo.siteadmin & 8) == 0) && ((message.event.account.siteadmin & 8) != 0))) { meshserver.send({ action: 'files' }); }
                                var oldgroups = userinfo.groups;
                                userinfo = message.event.account;
                                if ((oldsiteadmin != newsiteadmin) || (oldRemoveRights != newRemoveRights) || (message.event.accountImageChange == 1)) { // If the site admin permission or user image has changed...
                                    if (message.event.accountImageChange == 1) { userinfo.accountImageRnd = Math.floor(Math.random() * 9999999999); }
                                    updateSiteAdmin();
                                }
                                updateSelf();

                                if ((userinfo.siteadmin & 2) != 0) {
                                    // Compare our groups
                                    var og = oldgroups ? oldgroups : [];
                                    var ng = userinfo.groups ? userinfo.groups : [];
                                    if (og.join(',') != ng.join(',')) {
                                        // Our groups have changed, re-ask for a list of users.
                                        users = wssessions = null;
                                        meshserver.send({ action: 'users' });
                                        meshserver.send({ action: 'wssessioncount' });
                                    }
                                }

                                // If our list of nodes may have changes, request the new list now.
                                if (message.event.nodeListChange == userinfo._id) { meshserver.send({ action: 'nodes', skip: (devicePagingState == null) ? 0 : devicePagingState.skip }); }
                            }
                            if (currentNode) { refreshDevice(currentNode._id); }
                            if (users == null) break;

                            // Check if the account is part of our user group
                            if ((userinfo.groups == null) || (userinfo.groups.length == 0) || (findOne(message.event.account.groups, userinfo.groups) == true)) {
                                users[message.event.account._id] = message.event.account; // Part of our groups, update this user.
                            } else {
                                delete users[message.event.account._id]; // No longer part of our groups, remove this user.
                            }

                            mainUpdate(4 | 16384);
                            break;
                        }
                        case 'accountremove': {
                            // An account was removed
                            if (users == null) break;
                            delete users[message.event.userid];
                            mainUpdate(16384);
                            break;
                        }
                        case 'createusergroup':
                        case 'usergroupchange': {
                            // User group changed
                            if (usergroups == null) { usergroups = {}; }
                            var ugroup = usergroups[message.event.ugrpid];
                            if (ugroup == null) {
                                // This is a new user group for us
                                usergroups[message.event.ugrpid] = { _id: message.event.ugrpid, name: message.event.name, desc: message.event.desc, domain: message.event.domain, links: message.event.links };
                            } else {
                                // This is an existing user group
                                ugroup.name = message.event.name;
                                if (message.event.desc) { ugroup.desc = message.event.desc; } else { delete ugroup.desc; }
                                if (message.event.links) { ugroup.links = message.event.links; } else { delete ugroup.links; }
                                if (message.event.flags) { ugroup.flags = message.event.flags; } else { delete ugroup.flags; }
                                if (typeof message.event.consent == 'number') { ugroup.consent = message.event.consent; }
                            }
                            mainUpdate(4096 + 8192 + 16384);

                            // Group update, refresh all our device groups and nodes. TODO: Optimize this to only do this when needed.
                            meshserver.send({ action: 'meshes' });
                            meshserver.send({ action: 'nodes', skip: (devicePagingState == null) ? 0 : devicePagingState.skip });
                            break;
                        }
                        case 'deleteusergroup': {
                            // User group removed
                            if ((usergroups != null) && (usergroups[message.event.ugrpid] != null)) {
                                delete usergroups[message.event.ugrpid];
                                var c = 0;
                                for (var i in usergroups) { c++; }
                                if (c == 0) { usergroups = null; } // If user groups is empty, set it to null.
                                mainUpdate(8192 + 16384);
                            }
                            break;
                        }
                        case 'createmesh': {
                            // A new mesh was created
                            if ((meshes[message.event.meshid] == null) && ((serverinfo.manageAllDeviceGroups) || (message.event.mesh.links[userinfo._id] != null))) { // Check if this is a mesh create for a mesh we own. If site administrator, we get all messages so need to ignore some.
                                meshes[message.event.meshid] = message.event.mesh;
                                mainUpdate(4 + 128 + 8192 + 16384);
                                meshserver.send({ action: 'files' });
                            }
                            break;
                        }
                        case 'meshchange': {
                            // Update mesh information
                            if (meshes[message.event.meshid] == null) {
                                // Check if we have any access to this device group
                                var add = false;
                                if (message.event.links[userinfo._id] != null) { add = true; }
                                if (userinfo.links[message.event.meshid] != null) { add = true; }
                                for (var i in userinfo.links) { if ((i.startsWith('ugrp/')) && (message.event.links[i] != null)) { add = true; } }

                                // This is a new mesh for us
                                if (add) {
                                    meshes[message.event.meshid] = { _id: message.event.meshid, name: message.event.name, mtype: message.event.mtype, desc: message.event.desc, links: message.event.links, amt: message.event.amt, invite: message.event.invite, expireDevs: message.event.expireDevs, relayid: message.event.relayid };
                                    meshserver.send({ action: 'nodes', skip: (devicePagingState == null) ? 0 : devicePagingState.skip }); // Request a refresh of all nodes (TODO: We could optimize this to only request nodes for the new mesh).
                                }
                            } else {
                                // This is an existing device group
                                if (message.event.name != null) {
                                    meshes[message.event.meshid].name = message.event.name;
                                    for (var i in nodes) { if (nodes[i].meshid == message.event.meshid) { nodes[i].meshnamel = message.event.name.toLowerCase(); } }
                                }
                                if (message.event.desc != null) { meshes[message.event.meshid].desc = message.event.desc; }
                                if (message.event.flags != null) { meshes[message.event.meshid].flags = message.event.flags; }
                                if (message.event.consent != null) { meshes[message.event.meshid].consent = message.event.consent; }
                                if (message.event.links) { meshes[message.event.meshid].links = message.event.links; }
                                if (message.event.amt) { meshes[message.event.meshid].amt = message.event.amt; }
                                if (message.event.invite != null) { meshes[message.event.meshid].invite = message.event.invite; } else { delete meshes[message.event.meshid].invite; }
                                if (message.event.expireDevs != null) { if (message.event.expireDevs > 0) { meshes[message.event.meshid].expireDevs = message.event.expireDevs; } else { delete meshes[message.event.meshid].expireDevs; } }
                                if (message.event.relayid != null) { meshes[message.event.meshid].relayid = message.event.relayid; }

                                // Check if we lost rights to this mesh in this change.
                                if (IsMeshViewable(message.event.meshid) == false) {
                                    if ((xxcurrentView == 20) && (currentMesh == meshes[message.event.meshid])) go(2);
                                    delete meshes[message.event.meshid];

                                    // Delete all nodes in that mesh, except ones with direct links
                                    var newnodes = [];
                                    for (var i in nodes) { if ((nodes[i].meshid != message.event.meshid) || ((userinfo.links != null) && (userinfo.links[nodes[i]._id] != null))) { newnodes.push(nodes[i]); } }
                                    nodes = newnodes;

                                    // If we are looking at a node that is no longer visible, move back to "My Devices"
                                    if ((xxcurrentView >= 10) && (xxcurrentView < 20) && currentNode && !IsNodeViewable(currentNode)) { setDialogMode(0); go(1); }
                                }
                            }
                            mainUpdate(4 + 128 + 8192 + 16384);
                            if (currentNode && !IsNodeViewable(currentNode)) { currentNode = null; if ((xxcurrentView >= 10) && (xxcurrentView < 20)) { go(1); } }
                            //meshserver.send({ action: 'files' }); // TODO: Why do we need to do this??

                            // If we are looking at a mesh that is now deleted, move back to "My Account"
                            if (xxcurrentView == 20 && currentMesh._id == message.event.meshid) { mainUpdate(4096); }
                            break;
                        }
                        case 'deletemesh': {
                            // Delete the mesh
                            if (meshes[message.event.meshid]) {
                                delete meshes[message.event.meshid];
                                mainUpdate(128);
                                meshserver.send({ action: 'files' });
                            }

                            // Delete all nodes in that mesh
                            var newnodes = [];
                            if (nodes != null) { for (var i in nodes) { if (nodes[i].meshid != message.event.meshid) { newnodes.push(nodes[i]); } } }
                            nodes = newnodes;
                            mainUpdate(4 + 8192 + 16384);

                            // If we are looking at a mesh that is now deleted, move back to "My Account"
                            if (xxcurrentView >= 20 && xxcurrentView < 30 && currentMesh._id == message.event.meshid) { setDialogMode(0); go(2); }
                            // If we are looking at a node in the deleted mesh, move back to "My Devices"
                            if (xxcurrentView >= 10 && xxcurrentView < 20 && currentNode && !IsNodeViewable(currentNode)) { setDialogMode(0); go(1); }
                            break;
                        }
                        case 'addnode': {
                            var node = message.event.node;
                            if (!meshes[node.meshid]) break; // This is a node for a mesh we don't know. Happens when we are site administrator, we get all messages.
                            if (getNodeFromId(node._id) != null) break; // This node is already known.
                            node.namel = node.name.toLowerCase();
                            if (node.rname) { node.rnamel = node.rname.toLowerCase(); } else { node.rnamel = node.namel; }
                            node.meshnamel = meshes[node.meshid] ? meshes[node.meshid].name.toLowerCase() : '*';
                            node.state = 0;
                            if (!node.icon) node.icon = 1;
                            node.ident = ++nodeShortIdent;
                            if (nodes == null) { }
                            nodes.push(node);

                            // Web page update
                            mainUpdate(1 | 2 | 4 | 16);

                            // If we are looking at the device group for this device, update that.
                            if ((xxcurrentView == 20) && (currentMesh != null) && (currentMesh._id == node.meshid)) { mainUpdate(4096); }

                            break;
                        }
                        case 'removenode': {
                            var index = -1;
                            for (var i in nodes) { if (nodes[i]._id == message.event.nodeid) { index = i; break; } }
                            if (index != -1) {
                                var node = nodes[index];
                                if (currentNode == node) {
                                    if (xxcurrentView >= 10 && xxcurrentView < 20) { setDialogMode(0); go(1); }
                                    currentNode = null;
                                    // TODO: Correctly disconnect from this node (Desktop/Terminal/Files...)
                                }
                                nodes.splice(index, 1);

                                // Web page update
                                mainUpdate(4 | 16);

                                // If we are looking at the device group for this device, update that.
                                if ((xxcurrentView == 20) && (currentMesh != null) && (currentMesh._id == node.meshid)) { mainUpdate(4096); }
                            }
                            break;
                        }
                        case 'changenode': {
                            var index = -1;
                            for (var i in nodes) { if (nodes[i]._id == message.event.nodeid) { index = i; break; } }
                            if (index != -1) {
                                var node = nodes[index];

                                // Change the node
                                node.name = message.event.node.name;
                                node.rname = message.event.node.rname;
                                node.lusers = message.event.node.lusers;
                                node.upnusers = message.event.node.upnusers;
                                node.users = message.event.node.users;
                                node.host = message.event.node.host;
                                node.desc = message.event.node.desc;
                                node.ip = message.event.node.ip;
                                node.osdesc = message.event.node.osdesc;
                                node.publicip = message.event.node.publicip;
                                node.iploc = message.event.node.iploc;
                                node.wifiloc = message.event.node.wifiloc;
                                node.gpsloc = message.event.node.gpsloc;
                                node.tags = message.event.node.tags;
                                node.ssh = message.event.node.ssh;
                                node.rdp = message.event.node.rdp;
                                node.userloc = message.event.node.userloc;
                                node.rdpport = message.event.node.rdpport;
                                node.rfbport = message.event.node.rfbport;
                                node.sshport = message.event.node.sshport;
                                node.httpport = message.event.node.httpport;
                                node.httpsport = message.event.node.httpsport;
                                node.consent = message.event.node.consent;
                                node.pmt = message.event.node.pmt;
                                if (message.event.node.links != null) { node.links = message.event.node.links; } else { delete node.links; }
                                if (message.event.node.agent != null) {
                                    if (node.agent == null) node.agent = {};
                                    if (message.event.node.agent.ver != null) { node.agent.ver = message.event.node.agent.ver; }
                                    if (message.event.node.agent.id != null) { node.agent.id = message.event.node.agent.id; }
                                    if (message.event.node.agent.caps != null) { node.agent.caps = message.event.node.agent.caps; }
                                    if (message.event.node.agent.root != null) { node.agent.root = message.event.node.agent.root; }
                                    if (message.event.node.agent.core != null) { node.agent.core = message.event.node.agent.core; } else { if (node.agent.core) { delete node.agent.core; } }
                                    node.agent.tag = message.event.node.agent.tag;
                                }
                                if (message.event.node.intelamt != null) {
                                    if (node.intelamt == null) node.intelamt = {};
                                    if (message.event.node.intelamt.state != null) { node.intelamt.state = message.event.node.intelamt.state; }
                                    if (message.event.node.intelamt.host != null) { node.intelamt.user = message.event.node.intelamt.host; }
                                    if (message.event.node.intelamt.user != null) { node.intelamt.user = message.event.node.intelamt.user; } else { delete node.intelamt.user; }
                                    if (message.event.node.intelamt.tls != null) { node.intelamt.tls = message.event.node.intelamt.tls; }
                                    if (message.event.node.intelamt.ver != null) { node.intelamt.ver = message.event.node.intelamt.ver; }
                                    if (message.event.node.intelamt.tag != null) { node.intelamt.tag = message.event.node.intelamt.tag; }
                                    if (message.event.node.intelamt.uuid != null) { node.intelamt.uuid = message.event.node.intelamt.uuid; }
                                    if (message.event.node.intelamt.realm != null) { node.intelamt.realm = message.event.node.intelamt.realm; }
                                    if (message.event.node.intelamt.flags != null) { node.intelamt.flags = message.event.node.intelamt.flags; }
                                    if (message.event.node.intelamt.warn != null) { node.intelamt.warn = message.event.node.intelamt.warn; } else { delete node.intelamt.warn; }
                                }
                                if (message.event.node.av != null) { node.av = message.event.node.av; }
                                if (message.event.node.lsc != null) { node.lsc = message.event.node.lsc; }
                                if (message.event.node.wsc != null) { node.wsc = message.event.node.wsc; }
                                if (message.event.node.defender != null) { node.defender = message.event.node.defender; }
                                if (message.event.node.volumes != null) { node.volumes = message.event.node.volumes; }
                                node.namel = node.name.toLowerCase();
                                if (node.rname) { node.rnamel = node.rname.toLowerCase(); } else { node.rnamel = node.namel; }
                                if (message.event.node.icon) { node.icon = message.event.node.icon; }
                                if (message.event.node.lastbootuptime != null) { node.lastbootuptime = message.event.node.lastbootuptime; }
                                if (message.event.node.idletime != null) { node.idletime = message.event.node.idletime; }

                                // Check if this device has changed device group
                                if (message.event.node.meshid != node.meshid) {
                                    if ((meshes[message.event.node.meshid] == null) && ((userinfo.links == null) || (userinfo.links[node._id] == null))) {
                                        // We don't see the new mesh, remove this device
                                        // TODO: Correctly disconnect from this node (Desktop/Terminal/Files...)
                                        if ((currentNode != null) && (currentNode._id == node._id)) {
                                            if ((xxcurrentView >= 10) && (xxcurrentView < 20) && !IsNodeViewable(currentNode)) { currentNode = null; setDialogMode(0); go(1); }
                                        }
                                        var index = -1;
                                        for (var i in nodes) { if (nodes[i]._id == message.event.nodeid) { index = i; break; } }
                                        nodes.splice(index, 1);

                                        mainUpdate(4 | 16);
                                    } else {
                                        // We see the new mesh, move this device
                                        node.meshid = message.event.node.meshid;
                                        node.meshnamel = meshes[message.event.node.meshid] ? meshes[message.event.node.meshid].name.toLowerCase() : '*';
                                        mainUpdate(1 | 2 | 4);
                                    }
                                }

                                // Web page update
                                updateDeviceViewDevice(node);
                                mainUpdate(2 | 8 | 16);
                                refreshDevice(node._id);

                                // If we are looking at the device group for this device, update that.
                                if ((xxcurrentView == 20) && (currentMesh != null) && (currentMesh._id == node.meshid)) { mainUpdate(4096); }

                                if ((currentNode == node) && (xxdialogMode != null) && (xxdialogTag == '@xxmap')) { p10showNodeLocationDialog(); }
                            }
                            break;
                        }
                        case 'nodemeshchange': {
                            var index = -1;
                            for (var i in nodes) { if (nodes[i]._id == message.event.nodeid) { index = i; break; } }
                            if (index != -1) {
                                var node = nodes[index];
                                if ((meshes[message.event.newMeshId] == null) && ((userinfo.links == null) || (userinfo.links[node._id] == null))) {
                                    // We don't see the new mesh, remove this device

                                    // TODO: Correctly disconnect from this node (Desktop/Terminal/Files...)
                                    if ((currentNode != null) && (currentNode._id == node._id)) {
                                        if ((xxcurrentView >= 10) && (xxcurrentView < 20) && !IsNodeViewable(currentNode)) { currentNode = null; setDialogMode(0); go(1); }
                                    }
                                    nodes.splice(index, 1);
                                    mainUpdate(4 | 16);
                                } else {
                                    // We see the new mesh, move this device
                                    node.meshid = message.event.newMeshId;
                                    node.meshnamel = meshes[message.event.newMeshId] ? meshes[message.event.newMeshId].name.toLowerCase() : '*';
                                    mainUpdate(1 | 2 | 4);
                                }
                                refreshDevice(message.event.nodeid);
                            } else {
                                // This is a new device, add it.
                                var node = message.event.node;
                                if (!meshes[node.meshid]) break; // This is a node for a mesh we don't know. Happens when we are site administrator, we get all messages.
                                node.namel = node.name.toLowerCase();
                                if (node.rname) { node.rnamel = node.rname.toLowerCase(); } else { node.rnamel = node.namel; }
                                node.meshnamel = meshes[node.meshid] ? meshes[node.meshid].name.toLowerCase() : '*';
                                node.state = 0;
                                if (!node.icon) node.icon = 1;
                                node.ident = ++nodeShortIdent;
                                if (nodes == null) { }
                                nodes.push(node);

                                // Web page update
                                mainUpdate(1 | 2 | 4 | 16);
                            }
                            break;
                        }
                        case 'nodeconnect': {
                            // Indicated a node has changed connectivity state
                            var index = -1;
                            for (var i in nodes) { if (nodes[i]._id == message.event.nodeid) { index = i; break; } }
                            if (index != -1) {
                                var node = nodes[index];

                                // Event the connection change if needed
                                var n = getstore('notifications', 0); // Account notification settings

                                // Per-group notification settings
                                if (message.event.meshid && userinfo.links && userinfo.links[message.event.meshid] && userinfo.links[message.event.meshid].notify) {
                                    n |= userinfo.links[message.event.meshid].notify;
                                }

                                // Per-user notification settings
                                if (userinfo.notify != null) {
                                    if (userinfo.notify[message.event.meshid] != null) { n |= userinfo.notify[message.event.meshid]; }
                                    if (userinfo.notify[message.event.nodeid] != null) { n |= userinfo.notify[message.event.nodeid]; }
                                }

                                // Show the notification
                                if (n & 2) {
                                    var agentPrivilages = "Agente conectado";
                                    if ((node.agent != null) && (node.agent.root === false)) { agentPrivilages = "Agente conectado con permisos limitados"; }
                                    var abc = { title: node.name, icon: node.icon, nodeid: node._id, id: message.event.id };
                                    if (((node.conn & 1) == 0) && ((message.event.conn & 1) != 0)) { abc.text = agentPrivilages; addNotification(abc); }
                                    if (((node.conn & 2) == 0) && ((message.event.conn & 2) != 0)) { abc.text = "Intel AMT CIRA conectado"; addNotification(abc); }
                                    if (((node.conn & 4) == 0) && ((message.event.conn & 4) != 0)) { abc.text = "Intel AMT conectado"; addNotification(abc); }
                                    if (((node.conn & 16) == 0) && ((message.event.conn & 16) != 0)) { abc.text = "MQTT conectado"; addNotification(abc); }
                                }
                                if (n & 4) {
                                    var abc = { title: node.name, icon: node.icon, nodeid: node._id, id: message.event.id };
                                    if (((node.conn & 1) != 0) && ((message.event.conn & 1) == 0)) { abc.text = "Agente desconectado"; addNotification(abc); }
                                    if (((node.conn & 2) != 0) && ((message.event.conn & 2) == 0)) { abc.text = "Intel AMT CIRA desconectado"; addNotification(abc); }
                                    if (((node.conn & 4) != 0) && ((message.event.conn & 4) == 0)) { abc.text = "Intel AMT desconectado"; addNotification(abc); }
                                    if (((node.conn & 16) != 0) && ((message.event.conn & 16) == 0)) { abc.text = "MQTT desconectado"; addNotification(abc); }
                                }

                                // Change the node connection state
                                node.conn = message.event.conn;
                                node.pwr = message.event.pwr;
                                node.lastconnect = Date.now();

                                // Clear sesssion and battery information if needed
                                if ((node.conn & 1) == 0) { delete node.sessions; }

                                // Web page update
                                var filter = Q('DevFilterSelect').value;
                                if ((filter == 1) || (filter == 5)) {
                                    // We are looking at online or offline devices. This may change that state.
                                    mainUpdate(1 | 4 | 16);
                                } else {
                                    // We are looking at a different filter, just update that specific device.
                                    updateDeviceViewDevice(node);
                                    mainUpdate(1 | 16);
                                }
                                refreshDevice(node._id);

                                // If we are looking at the device group for this device, update that.
                                if ((xxcurrentView == 20) && (currentMesh != null) && (currentMesh._id == node.meshid)) { mainUpdate(4096); }
                            }
                            break;
                        }
                        case 'wssessioncount': {
                            // Update the active web socket session count for a user
                            if (wssessions != null) {
                                if (message.event.count == 0 && wssessions[message.event.userid]) {
                                    delete wssessions[message.event.userid];
                                } else {
                                    wssessions[message.event.userid] = message.event.count;
                                }
                                mainUpdate(16384);
                            }
                            break;
                        }
                        case 'login': {
                            // Update the last login time
                            if (users != null && users[message.event.userid]) {
                                users[message.event.userid].login = Math.floor(new Date(message.event.time).getTime() / 1000);
                            }
                            break;
                        }
                        case 'scanamtdevice': {
                            // Populate the Intel AMT scan dialog box with the result of the RMCP scan
                            if ((xxdialogMode == null) || (!Q('dp1range')) || (xxdialogTag != ('AMTSCAN:' + message.event.range))) return;
                            var x = '';
                            if (message.event.results == null) {
                                // The scan could not occur because of an error. Likely the user range was invalid.
                                x = '<div style=width:100%;text-align:center;margin-top:12px>' + "No se puede escanear este rango de direcciones." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Valores de rango de IP de muestra" + '<br />192.168.0.100<br />192.168.1.0/24<br />192.167.0.1-192.168.0.100</div>';
                            } else {
                                // Go thru all the results and populate the dialog box
                                amtScanResults = message.event.results;
                                for (var i in message.event.results) {
                                    var r = message.event.results[i], shortname = r.hostname;
                                    if (r.hosttype == 'host') { shortname = capitalizeFirstLetter(shortname.split('.')[0]); }
                                    if (shortname.length > 20) { shortname = shortname.substring(0, 20) + '...'; }
                                    var str = '<b title="' + EscapeHtml(r.hostname) + '">' + EscapeHtml(shortname) + '</b> - v' + r.ver;
                                    if (r.state == 2) { if (r.tls == 1) { str += " con TLS."; } else { str += " sin TLS."; } } else { str += ' not activated.'; }
                                    x += '<div style=width:100%;margin-bottom:2px;background-color:lightgray><div style=padding:4px><div style=display:inline-block;margin-right:5px><input class="DevScanCheckbox form-check-input me-2" name=dp1checkbox tag="' + EscapeHtml(i) + '" type=checkbox onclick=addAmtScanToMeshCheckbox() /></div><div class=j1 style=display:inline-block></div><div style=display:inline-block;margin-left:5px;overflow-x:auto;white-space:nowrap>' + str + '</div></div></div>';
                                }
                                // If no results where found, display a nice message
                                if (x == '') { x = '<div style=width:100%;text-align:center;margin-top:12px>' + "La búsqueda no arrojó resultados." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Valores de rango de IP de muestra" + '<br />192.168.0.100<br />192.168.1.0/24<br />192.167.0.1-192.168.0.100</div>'; }
                            }
                            // Set the html in the dialog box and re-enable the scan button
                            QH('dp1results', x);
                            QE('dp1range', true);
                            QE('dp1rangebutton', true);
                            QE('d2scanSelectButton', true);
                            break;
                        }
                        case 'notify': {
                            var n = { text: message.event.value, title: message.event.title, icon: message.event.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
                            if (message.id != null) { n.id = message.id; }
                            if (message.event.tag != null) { n.tag = message.event.tag; }
                            if (typeof message.maxtime == 'number') { n.maxtime = message.maxtime; }
                            addNotification(n);
                            break;
                        }
                        case 'traceinfo': {
                            if (typeof message.event.traceSources == 'object') {
                                if ((message.event.traceSources != null) && (message.event.traceSources.length > 0)) {
                                    serverTraceSources = message.event.traceSources;
                                    QH('p41traceStatus', EscapeHtml(message.event.traceSources.join(', ')));
                                } else {
                                    serverTraceSources = [];
                                    QH('p41traceStatus', "Ninguno");
                                }
                            }
                            break;
                        }
                        case 'sysinfohash': {
                            // If the sysinfo document has changed and we are looking at it, request an update.
                            if ((currentNode != null) && (message.event.nodeid == powerTimelineReq)) {
                                meshserver.send({ action: 'getsysinfo', nodeid: message.event.nodeid });
                            }
                            break;
                        }
                        case 'ifchange': {
                            // Network interface changed for a device, if we are currently viewing this device, ask for an update.
                            if ((currentNode != null) && (currentNode._id == message.event.nodeid)) { meshserver.send({ action: 'getnetworkinfo', nodeid: currentNode._id }); }
                            break;
                        }
                        case 'devicesessions': {
                            // List of sessions for a given device
                            var node = getNodeFromId(message.event.nodeid);
                            if (node == null) break; // Unknown node
                            node.sessions = message.event.sessions;
                            if (node.sessions != null) {
                                for (var i in node.sessions) { if (Object.keys(node.sessions[i]).length == 0) { delete node.sessions[i]; } }
                                if (Object.keys(node.sessions).length == 0) { delete node.sessions; }
                            }
                            updateDeviceViewDevice(node);
                            if ((currentNode != null) && (currentNode._id == message.event.nodeid)) { gotoDevice(currentNode._id, xxcurrentView, true); }

                            // If we are looking at the sessions dialog box for this device now, update it
                            if (xxdialogTag == ('SESSIONS-' + message.event.nodeid)) { showDeviceSessions(message.event.nodeid, true); }
                            if (xxdialogTag == ('MESSAGES-' + message.event.nodeid)) { showDeviceMessages(message.event.nodeid, true); }
                            if (xxdialogTag == ('HELPREQ-' + message.event.nodeid)) { showDeviceHelpRequests(message.event.nodeid, true); }

                            // If we are filtering on sessions or help, update the visible devices
                            if ((Q('DevFilterSelect').value == 2) || (Q('DevFilterSelect').value == 6)) { mainUpdate(1); }
                            break;
                        }
                        case 'loginTokenChanged': { // Login tokens have changed
                            if (message.event.userid != userinfo._id) return;
                            loginTokens = message.event.loginTokens;
                            mainUpdate(65536);
                            break;
                        }
                        case 'loginTokenAdded': { // A login token was added
                            if (message.event.userid != userinfo._id) return;
                            if (loginTokens == null) { loginTokens = []; }
                            loginTokens.push(message.event.newToken);
                            mainUpdate(65536);
                            break;
                        }
                        case 'stopped': { // Server is stopping.
                            // Disconnect
                            //console.log(message.msg);
                            break;
                        }
                        case 'updatePluginList': {
                            installedPluginList = message.event.list;
                            updatePluginList();
                            break;
                        }
                        case 'pluginStateChange': {
                            if (pluginHandler == null) break;
                            pluginHandler.refreshPluginHandler();
                            break;
                        }
                        case 'plugin': {
                            if (pluginHandler == null) break;
                            try { pluginHandler[message.event.plugin][message.event.pluginaction](message); } catch (e) { console.log("PluginHandler no pudo enviar el mensaje de evento:", e); }
                            break;
                        }
                        default:
                            //console.log('Unknown message.event.action', message.event.action);
                            break;
                    }
                    break;
                }
                case 'pluginPermissions': {
                    handlePluginPermissions(message);
                    break;
                }
                case 'pluginPermissionsSet': {
                    if (message.success) {
                        alert("Permisos guardados correctamente");
                    } else {
                        alert("Error al guardar los permisos: " + message.error);
                    }
                    break;
                }
                case 'pluginPermissionList': {
                    handlePluginPermissionList(message);
                    break;
                }
                case 'createInviteLink': { // Agent installation invitation link
                    if (xxdialogTag != message.meshid) break;
                    var servername = serverinfo.name;
                    if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
                    var domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1);
                    var url;
                    if (serverinfo.https == true) {
                        var portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port);
                        url = 'https://' + servername + portStr + domainUrl + 'agentinvite?c=' + message.cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
                    } else {
                        var portStr = (serverinfo.port == 80) ? '' : (':' + serverinfo.port);
                        url = 'http://' + servername + portStr + domainUrl + 'agentinvite?c=' + message.cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
                    }
                    Q('agentInvitationLink').href = url;
                    var t = format((message.expire == 1) ? "1 hora" : "{0} horas", message.expire);
                    if (message.expire == 24) { t = "1 dia"; }
                    if (message.expire == 168) { t = "1 semana"; }
                    if (message.expire == 5040) { t = "1 mes"; }
                    if (message.expire == 0) { t = "Ilimitado"; }
                    QH('agentInvitationLink', format("Enlace de Invitación ({0})", t));
                    QV('agentInvitationLinkDiv', true);
                    break;
                }
                case 'createDeviceShareLink': { // Guest sharing link
                    var node = getNodeFromId(message.nodeid), x = '';
                    if (node == null) break;
                    x += addHtmlValue("Dispositivo", EscapeHtml(node.name));
                    x += addHtmlValue("Nombre de Invitado", message.guestname);
                    x += addHtmlValue("Entrada del Ssuario", message.viewOnly ? "No permitido, solo vista" : "Permitido");
                    if (message.start && message.expire) {
                        if (message.recurring) {
                            x += addHtmlValue("Tiempo de Inicio", printDateTime(new Date(message.start)));
                            if (message.expire < 2) { x += addHtmlValue("Duración", format("{0} minuto", message.expire)); }
                            else { x += addHtmlValue("Duración", format("{0} minutos", message.expire)); }
                        } else {
                            x += addHtmlValue("Tiempo de Inicio", printDateTime(new Date(message.start)));
                            x += addHtmlValue("Tiempo de Expiración", printDateTime(new Date(message.expire)));
                        }
                    }
                    if (message.recurring == 1) { x += addHtmlValue("Periódico", "A diario"); }
                    if (message.recurring == 2) { x += addHtmlValue("Periódico", "Semanalmente"); }
                    var y = [];
                    if (message.consent & 0x0007) { y.push("Notificar"); }
                    if (message.consent & 0x0038) { y.push("Solicitar"); }
                    if (message.consent & 0x0040) { y.push("Barra de privacidad"); }
                    if (y.length == 0) { y.push("Ninguno"); }
                    x += addHtmlValue("Consentimiento del Usuario", y.join(', '));
                    var type = ''; if (message.p <= 7) { type = ['', "Enlace de Terminal Remota", "Enlace de Escritorio Remoto", "Escritorio Remoto + enlace de terminal", "Enlace de Archivos Remotos", "Terminal Remota + Enlace de Archivos", "Escritorio Remoto + Enlace de Archivos", "Escritorio Remoto + Terminal + Enlace de archivos"][message.p]; } else if (message.p == 8) { type = format("HTTP/{0} link", message.port); } else if (message.p == 16) { type = format("HTTPS/{0}", message.port); }
                    x += '<div id=agentInvitationLinkDiv style="text-align:center;font-size:large;margin:16px"><a href="' + message.url + '" id=agentInvitationLink rel="noreferrer noopener" target="_blank" style=cursor:pointer>' + type + '</a> <i class="fa-regular fa-copy" title="' + "Copiar enlance al portapapeles" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></i></div></div>';
                    setModalContent('xxAddAgent', "Compartir Dispositivo", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getmqttlogin': {
                    if ((currentNode == null) || (currentNode._id != message.nodeid) || (xxdialogMode != null)) return;
                    var x = "Esta configuración se puede utilizar para conectar MQTT para este dispositivo." + '<br /><br />';
                    delete message.action;
                    delete message.nodeid;
                    x += '<textarea readonly=readonly style=width:100%;resize:none;height:100px;overflow:auto;font-size:12px readonly>' + JSON.stringify(message) + '</textarea>';
                    /*
                    x += addHtmlValue('Username', '<input style=width:230px readonly value="' + message.user + '" />');
                    x += addHtmlValue('Password', '<input style=width:230px readonly value="' + message.pass + '" />');
                    x += addHtmlValue('WS URL', '<input style=width:230px readonly value="' + message.wsUrl + '" />');
                    if (message.mpsUrl && message.mpsCertHash) {
                        x += addHtmlValue('MPS URL', '<input style=width:230px readonly value="' + message.mpsUrl + '" />');
                        x += addHtmlValue('MPS Cert Hash', '<input style=width:230px readonly value="' + message.mpsCertHash + '" />');
                    }
                    */
                    setModalContent('xxAddAgent', "Credenciales MQTT", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'stopped': { // Server is stopping.
                    // Disconnect
                    autoReconnect = false;
                    QH('p0span', message.msg);
                    break;
                }
                case 'updatePluginList': {
                    installedPluginList = message.list;
                    updatePluginList();
                    break;
                }
                case 'pluginVersionsAvailable': {
                    if (pluginHandler == null) break;
                    updatePluginList(message.list);
                    break;
                }
                case 'downgradePluginVersions': {
                    var vSelect = '<select id="lastPluginVersion">';
                    message.info.versionList.forEach(function (v) { vSelect += '<option value="' + v.zipball_url + '">' + v.name + '</option>'; });
                    vSelect += '</select>';
                    setModalContent('xxAddAgent', "Acción de Plugin", format('Select the version to downgrade the plugin: {0}', message.info.name) + '<hr />' + vSelect + '<hr />' + "Ten en cuenta que no se recomienda bajar de versión. Solo hazlo en caso de que una actualización reciente haya roto algo." + + '<input id="lastPluginAct" type="hidden" value="downgrade" /><input id="lastPluginId" type="hidden" value="' + message.info.id + '" />');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', pluginActionEx);
                    break;
                }
                case 'serverBackup': {
                    if (message.service == 'googleDrive') {
                        QV('p2ServerActionsGoogleBackup', message.state > 0);
                        QV('p2ServerActionsGoogleBackupCheck', message.state > 2);
                        miscState['googleDrive'] = { state: message.state }
                        if (message.state == 2) {
                            var x = '<img style=float:right src="images/googledrive-48.png" /><div>' + "Navega a la siguiente URL, confirma el acceso y copie nuevamente el código token." + '</div><br />';
                            x += addHtmlValue("Activación", '<a href=\"' + message.url + '\" rel="noreferrer noopener" target="_blank">Browse to this URL</a>');
                            x += addHtmlFormFloating("Código", '<input id=gdcode onkeyup=server_setupGoogleDriveBackupCheck3() class=form-control></input>');
                            setModalContent('xxAddAgent', "Resguardo en Google Drive", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_setupGoogleDriveBackupEx(3, 'gd2'));
                            Q('gdcode').focus();
                            QE('idx_dlgOkButton', false);
                        }
                    }
                    break;
                }
                case 'pluginError': {
                    setModalContent('xxAddAgent', "Error en el Plugin", message.msg);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'plugin': {
                    if ((pluginHandler == null) || (typeof message.plugin != 'string')) break;
                    try { pluginHandler[message.plugin][message.method](server, message); } catch (e) { console.log('Error loading plugin handler (' + e + ')'); }
                    break;
                }
                case 'amtsetupbin': {
                    saveAs(new Blob([Uint8Array.from(atob(message.file), function (c) { return c.charCodeAt(0) })], { type: 'application/octet-stream' }), 'setup.bin');
                    break;
                }
                case 'previousLogins': {
                    var tag = 'previousLogins';
                    if (message.userid != null) { tag += ':' + message.userid; }
                    var x = '', c = 'BBB', xx = '';
                    if (message.events.length == 0) {
                        x += 'No previous login.';
                    } else {
                        x += '<div class="backgroundContainer" style=max-height:260px;overflow-y:scroll;overflow-x:hidden><table>';
                        for (var i in message.events) {
                            var m = message.events[i].m;
                            if (m == 107) { m = "Inicio de sesión válido"; c = 'BBD1BB'; xx = ''; if (message.events[i].tn != null) { m = format("Token: {0}", message.events[i].tn); c = '88D188' } }
                            else if (m == 108) { m = "2FA no válido"; c = 'DD9DC3'; xx = 'x'; }
                            else if (m == 109) { m = "Cuenta bloqueada"; c = 'E1BBBB'; xx = 'x'; }
                            else if (m == 110) { m = "Contraseña no válida"; c = 'E1BBBB'; xx = 'x'; }
                            x += '<tr><td><img src=images/user-32' + xx + '.png height=32 width=32 style=float:left srcset="images/user-64' + xx + '.png 2x"><td><div style=width:350px;background-color:#' + c + ';border-radius:6px;margin-bottom:4px;padding:4px><div>' + printDateTime(new Date(message.events[i].t)) + ', <b>' + EscapeHtml(m) + '</b></div><div style=font-size:x-small>' + EscapeHtml(message.events[i].a.join(', ')) + '</div></div></tr>';
                        }
                        x += '</table></div>';
                    }
                    setModalContent('xxAddAgent', "Inicios de Sesión Anteriores", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getDeviceDetails': {
                    saveAs(new Blob([message.data], { type: 'application/octet-stream' }), "lista de dispositivos" + '.' + message.type);
                    break;
                }
                case 'createLoginToken': { // A new login token was created
                    if (xxdialogMode) return;
                    var x = "Toma nota de este nombre de usuario y contraseña, la contraseña no se puede volver a mostrar." + '<br /><br />';
                    x += addHtmlValue("Nombre", EscapeHtml(message.name))
                    if (message.expire != 0) { x += addHtmlValue("Expirar", EscapeHtml(printDateTime(new Date(message.expire)))); }
                    x += addHtmlValue("Nombre de Usuario", '<i class="fa-regular fa-copy" title="' + "Copiar enlance al portapapeles" + '" style="margin:9px;cursor:pointer;float:right" onclick=copyTextToClip2("' + encodeURIComponentEx(message.tokenUser) + '")></i><div class=selecttext style=width:230px;padding:5px;border-radius:5px;font-size:15px>' + EscapeHtml(message.tokenUser) + '</div>');
                    x += addHtmlValue("Contraseña", '<i class="fa-regular fa-copy" title="' + "Copiar enlance al portapapeles" + '" style="margin:9px;cursor:pointer;float:right" onclick=copyTextToClip2("' + encodeURIComponentEx(message.tokenPass) + '")></i><div class=selecttext style=width:230px;padding:5px;border-radius:5px;font-size:15px>' + EscapeHtml(message.tokenPass) + '</div>');
                    setModalContent('xxAddAgent', "Token de inicio de sesión creado", x);
                    QV('idx_dlgOkButton', false);
                    Q('idx_dlgCancelButton').innerHTML = "Cerrar";
                    break;
                }
                case 'loginTokens': { // Reveiced the list of login tokens
                    loginTokens = message.loginTokens;
                    mainUpdate(65536);
                    break;
                }
                case 'report': {
                    renderReport(message.data);
                    break;
                }
                default:
                    //console.log('Unknown message.action', message.action);
                    break;
            }
        }

        // Go to the correct starting view page
        function gotoStartViewPage() {
            var xviewmode = parseInt('{{viewmode}}');
            if (xxcurrentView != -1) return;
            if ('{{currentNode}}'.toLowerCase() != '') { // The .toLowerCase here is the minifier will not optimize this out.
                if (getNodeFromId('{{currentNode}}') == null) return; // This node is not loaded yet
                gotoDevice('{{currentNode}}', xviewmode);
            } else if (args.gotonode != null) {
                if (args.gotonode.length == 96) { args.gotonode = btoa(hex2rstr(args.gotonode)).split('+').join('@').split('/').join('$'); } // This is a HEX encoded NodeID, convert it to Base64
                if (getNodeFromId('node/' + domain + '/' + args.gotonode) == null) return; // This node is not loaded yet
                gotoDevice('node/' + domain + '/' + args.gotonode, xviewmode);
                goBackStack.push(1);
            } else if (args.gotodevicename != null) {
                var foundNode = null;
                if (nodes != null) { for (var i in nodes) { if (nodes[i].name == args.gotodevicename) { foundNode = nodes[i]._id; } } }
                if (foundNode) { gotoDevice(foundNode, xviewmode); goBackStack.push(1); }
            } else if (args.gotodevicername != null) {
                var foundNode = null;
                if (nodes != null) { for (var i in nodes) { if (nodes[i].rname == args.gotodevicername) { foundNode =  nodes[i]._id; } } }
                if (foundNode) { gotoDevice(foundNode, xviewmode); goBackStack.push(1); }
            } else if (args.gotodeviceip != null) {
                var foundNode = null;
                if (nodes != null) { for (var i in nodes) { if (nodes[i].ip == args.gotodeviceip) { foundNode =  nodes[i]._id; } } }
                if (foundNode) { gotoDevice(foundNode, xviewmode); goBackStack.push(1); }
            } else if (args.gotomesh != null) {
                if (meshes['mesh/' + domain + '/' + args.gotomesh] == null) return; // This device group is not loaded yet
                gotoMesh('mesh/' + domain + '/' + args.gotomesh);
                go(xviewmode);
                goBackStack.push(2);
            } else if (args.gotouser != null) {
                var xuserid = args.gotouser;
                if (args.gotouser.indexOf('/') < 0) { xuserid = 'user/' + domain + '/' + args.gotouser; }
                if ((users == null) || (users[xuserid] == null)) return; // This user is not loaded yet
                gotoUser(encodeURIComponentEx(xuserid));
                go(xviewmode);
                goBackStack.push(4);
            } else if (args.gotougrp != null) {
                var xusergrpid = args.gotougrp;
                if (args.gotougrp.indexOf('/') < 0) { xusergrpid = 'ugrp/' + domain + '/' + args.gotougrp; }
                if ((usergroups == null) || usergroups[xusergrpid] == null) return; // This user group is not loaded yet
                gotoUserGroup(xusergrpid);
                go(xviewmode);
                goBackStack.push(50);
            } else if (!isNaN(xviewmode)) {
                go(xviewmode);
            } else {
                setDialogMode(0);
                go(1);
            }
            delete args.gotonode;
            delete args.gotomesh;
            delete args.gotouser;
            delete args.gotougrp;
        }

        //
        // MY DEVICES
        //


        function onRealNameCheckBox() {
            showRealNames = Q('RealNameCheckBox').checked;
            putstore('showRealNames', showRealNames ? 1 : 0);
            mainUpdate(7);
        }

        function onOnlineCheckBox(e) {
            putstore('devFilterSelect', Q('DevFilterSelect').value);
            onDeviceSearchChanged(e);
        }

        function updateDevicePageState() {
            if ((devicePagingState == null) || (devicePagingState.total <= devicePagingState.limit)) {
                QV('devViewPageState', false);
                QV('devViewPageButton1', false);
                QV('devViewPageButton2', false);
                QV('devViewPageButton3', false);
                QV('devViewPageButton4', false);
            } else {
                var currentPage = Math.floor((devicePagingState.skip + devicePagingState.limit) / devicePagingState.limit);
                var maxPage = Math.ceil(devicePagingState.total / devicePagingState.limit);
                QV('devViewPageState', true);
                QV('devViewPageButton1', true);
                QV('devViewPageButton2', true);
                QV('devViewPageButton3', true);
                QV('devViewPageButton4', true);
                QH('devViewPageState', currentPage + '/' + maxPage);
            }
        }

        function onDeviceViewPageChange(i) {
            if (devicePagingState == null) return;
            var currentPage = (Math.floor((devicePagingState.skip + devicePagingState.limit) / devicePagingState.limit));
            var maxPage = Math.ceil(devicePagingState.total / devicePagingState.limit);
            switch (i) {
                case 1: { if (currentPage > 1) meshserver.send({ action: 'nodes', skip: 0 }); break; } // Goto first page
                case 2: { if (currentPage > 1) meshserver.send({ action: 'nodes', skip: (currentPage - 2) * devicePagingState.limit }); break; } // Goto previous page
                case 3: { if (currentPage < maxPage) meshserver.send({ action: 'nodes', skip: currentPage * devicePagingState.limit }); break; } // Goto next page
                case 4: { if (currentPage < maxPage) meshserver.send({ action: 'nodes', skip: (maxPage - 1) * devicePagingState.limit }); break; } // Goto last page
            }
        }

        function onDeviceViewChange(i) {
            if (i != null) { Q('viewselect').value = i; }
            for (var j = 1; j < 6; j++) { Q('devViewButton' + j).classList.remove('viewSelectorSel'); }
            Q('devViewButton' + Q('viewselect').value).classList.add('viewSelectorSel');
            putstore('deviceView', Q('viewselect').value);
            putstore('viewsize', Q('sizeselect').value);
            mainUpdate(4);
            setTimeout(function () { mainUpdate(512); }, 200);
        }

        function onDeviceViewSettings() {
            if (xxdialogMode) return;

            // Use defaults if needed
            if (deviceViewSettings == null) { deviceViewSettings = {}; }
            if (!Array.isArray(deviceViewSettings.devsCols)) { deviceViewSettings.devsCols = ['user', 'ip', 'conn']; }

            // Display the dialog box
            var x = '';
            x += '<label><input id=d2c8 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('agtype') >= 0) ? ' checked' : '') + '>' + "Tipo de agente" + '</label><br />';
            x += '<label><input id=d2c9 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('agver') >= 0) ? ' checked' : '') + '>' + "Versión del agente" + '</label><br />';
            x += '<label><input id=d2c6 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('desc') >= 0) ? ' checked' : '') + '>' + "Descripción del aparato" + '</label><br />';
            x += '<label><input id=d2c5 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('os') >= 0) ? ' checked' : '') + '>' + "Sistema Operativo" + '</label><br />';
            x += '<label><input id=d2c10 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('tags') >= 0) ? ' checked' : '') + '>' + "Etiquetas" + '</label><br />';
            x += '<label><input id=d2c11 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsav') >= 0) ? ' checked' : '') + '>' + "Windows AV" + '</label><br />';
            x += '<label><input id=d2c12 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) ? ' checked' : '') + '>' + "Windows Update" + '</label><br />';
            x += '<label><input id=d2c13 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) ? ' checked' : '') + '>' + "Windows Firewall" + '</label><br />';
            x += '<label><input id=d2c14 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) ? ' checked' : '') + '>' + "Last Boot Up Time" + '</label><br />';
            x += '<label><input id=d2c16 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('idletime') >= 0)?' checked':'') + '>' + "Tiempo de inactividad" + '</label><br />';
            x += '<label><input id=d2c1 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('links') >= 0) ? ' checked' : '') + '>' + "Enlaces de enrutador MeshCentral" + '</label><br />';
            x += '<label><input id=d2c2 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('user') >= 0) ? ' checked' : '') + '>' + "Usuarios con sesión iniciada" + '</label><br />';
            x += '<label><input id=d2c3 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('ip') >= 0) ? ' checked' : '') + '>' + "Dirección IP del agente" + '</label><br />';
            x += '<label><input id=d2c4 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('conn') >= 0) ? ' checked' : '') + '>' + "Conectividad del servidor" + '</label><br />';
            x += '<label><input id=d2c15 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amthost') >= 0) ? ' checked' : '') + '>' + "Intel&reg; AMT hostname" + '</label><br />';
            x += '<label><input id=d2c17 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amtstate') >= 0) ? ' checked' : '') + '>' + "Intel&reg; AMT state" + '</label><br />';
            x += '<label><input id=d2c7 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastseen') >= 0) ? ' checked' : '') + '>' + "Ultima vez visto" + '</label><br />';
            setModalContent('xxAddAgent', "Columnas de vista de dispositivo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => onDeviceViewSettingsEx());
        }

        function onDeviceViewSettingsEx() {
            var cols = [];
            if (Q('d2c1').checked) { cols.push('links'); }
            if (Q('d2c2').checked) { cols.push('user'); }
            if (Q('d2c3').checked) { cols.push('ip'); }
            if (Q('d2c4').checked) { cols.push('conn'); }
            if (Q('d2c5').checked) { cols.push('os'); }
            if (Q('d2c6').checked) { cols.push('desc'); }
            if (Q('d2c7').checked) { cols.push('lastseen'); }
            if (Q('d2c8').checked) { cols.push('agtype'); }
            if (Q('d2c9').checked) { cols.push('agver'); }
            if (Q('d2c10').checked) { cols.push('tags'); }
            if (Q('d2c11').checked) { cols.push('windowsav'); }
            if (Q('d2c12').checked) { cols.push('windowsupdate'); }
            if (Q('d2c13').checked) { cols.push('windowsfirewall'); }
            if (Q('d2c14').checked) { cols.push('lastbootuptime'); }
            if (Q('d2c15').checked) { cols.push('amthost'); }
            if (Q('d2c16').checked) { cols.push('idletime'); }
            if (Q('d2c17').checked) { cols.push('amtstate'); }
            deviceViewSettings.devsCols = cols;
            putstore('_deviceViewSettings', JSON.stringify(deviceViewSettings));
            mainUpdate(4);
        }

        function ondockeypress(e) {
            if (document.querySelector('.modal.show')) {
                return;
            }
            mobileKbdGotKeypress = true; // tells onSoftKeyboardInput not to double-send
            setSessionActivity();
            if (!xxdialogMode && (xxcurrentView == 11) && desktop && Q('DeskControl').checked) {
                // Check what keys we are allows to send
                if (currentNode != null) {
                    var meshrights = GetNodeRights(currentNode);
                    var inputAllowed = ((features2 & 0x2000) == 0) && ((meshrights == 0xFFFFFFFF) || (((meshrights & 8) != 0) && ((meshrights & 256) == 0)));
                    if (inputAllowed == false) return false;
                    var limitedInputAllowed = ((meshrights != 0xFFFFFFFF) && (((meshrights & 8) != 0) && ((meshrights & 256) == 0) && ((meshrights & 4096) != 0)));
                    if (limitedInputAllowed == true) { if ((e.altKey == true) || (e.ctrlKey == true) || ((e.keyCode < 32) && (e.keyCode != 8) && (e.keyCode != 13)) || (e.keyCode > 90)) return false; }
                }
                return desktop.m.handleKeys(e);
            }
            if (!xxdialogMode && (xxcurrentView == 12) && terminal && (terminal.State == 3) && (xterm == null)) { return terminal.m.TermHandleKeys(e); }
            if (!xxdialogMode && ((xxcurrentView == 15) || (xxcurrentView == 115))) return agentConsoleHandleKeys(e);
            if (!xxdialogMode && xxcurrentView == 4) {
                if ((e.ctrlKey == true) || (e.altKey == true) || (e.metaKey == true)) return;
                var processed = 0;
                if (e.key) {
                    if ((e.key.length === 1) && (userSearchFocus == 0)) { Q('UserSearchInput').value = ((Q('UserSearchInput').value + e.key)); processed = 1; }
                    if ((e.keyCode == 8) && (userSearchFocus == 0)) { var x = Q('UserSearchInput').value; Q('UserSearchInput').value = x.substring(0, x.length - 1); processed = 1; }
                    if (e.keyCode == 27) { Q('UserSearchInput').value = ''; processed = 1; }
                } else {
                    if (e.charCode != 0 && userSearchFocus == 0) { Q('UserSearchInput').value = ((Q('UserSearchInput').value + String.fromCharCode(e.charCode))); processed = 1; }
                }
                if (processed > 0) { if (processed == 1) { onUserSearchInputChanged(); } return haltEvent(e); }
            }
            if (xxdialogMode || xxcurrentView != 1) return;
            if (e.ctrlKey == true && e.charCode == 96) {
                showRealNames = !showRealNames;
                Q('RealNameCheckBox').value = showRealNames;
                putstore('showRealNames', showRealNames ? 1 : 0);
                mainUpdate(6)
                return;
            }
            if (e.ctrlKey == true || e.altKey == true || e.metaKey == true) return;
            if (Q('viewselect').value < 4) {
                var processed = 0;
                if (e.key) {
                    if ((e.key.length === 1) && (searchFocus == 0)) { Q('KvmSearchInput').value = Q('SearchInput').value = ((Q('SearchInput').value + e.key)); processed = 1; }
                    if ((e.keyCode == 8) && (searchFocus == 0)) { var x = Q('SearchInput').value; Q('KvmSearchInput').value = Q('SearchInput').value = x.substring(0, x.length - 1); processed = 1; }
                    if (e.keyCode == 27) { Q('KvmSearchInput').value = Q('SearchInput').value = ''; processed = 1; }
                } else {
                    if (e.charCode != 0 && searchFocus == 0) { Q('KvmSearchInput').value = Q('SearchInput').value = ((Q('SearchInput').value + String.fromCharCode(e.charCode))); processed = 1; }
                }
                if (processed > 0) { if (processed == 1) { mainUpdate(5); } return haltEvent(e); }
            }
            if (Q('viewselect').value == 4) {
                if (e.key) {
                    if ((e.key.length === 1) && (mapSearchFocus == 0)) { Q('mapSearchLocation').value = ((Q('mapSearchLocation').value + e.key)); processed = 1; }
                    //if (e.keyCode == 8 && mapSearchFocus == 0) { var x = Q('mapSearchLocation').value; Q('mapSearchLocation').value = x.substring(0, x.length - 1); processed = 1; }
                    if (e.keyCode == 27) { Q('mapSearchLocation').value = ''; mapCloseSearchWindow(); processed = 1; }
                    if (e.keyCode == 13) { getSearchLocation(); }
                } else {
                    if (e.charCode != 0 && mapSearchFocus == 0) { Q('mapSearchLocation').value = ((Q('mapSearchLocation').value + String.fromCharCode(e.charCode))); processed = 1; }
                }
            }
        }

        function ondockeydown(e) {
            setSessionActivity();
            if (!xxdialogMode && (xxcurrentView == 11) && desktop && Q('DeskControl').checked) {
                // Check what keys we are allows to send
                if (currentNode != null) {
                    var meshrights = GetNodeRights(currentNode);
                    var inputAllowed = ((features2 & 0x2000) == 0) && ((meshrights == 0xFFFFFFFF) || (((meshrights & 8) != 0) && ((meshrights & 256) == 0)));
                    if (inputAllowed == false) return false;
                    var limitedInputAllowed = ((meshrights != 0xFFFFFFFF) && (((meshrights & 8) != 0) && ((meshrights & 256) == 0) && ((meshrights & 4096) != 0)));
                    if (limitedInputAllowed == true) { if ((e.altKey == true) || (e.ctrlKey == true) || ((e.keyCode < 32) && (e.keyCode != 8) && (e.keyCode != 13)) || (e.keyCode > 90)) return false; }
                }
                return desktop.m.handleKeyDown(e);
            }
            if (!xxdialogMode && (xxcurrentView == 12) && terminal && (terminal.State == 3) && xterm == null) { terminal.m.TermHandleKeyDown(e); if ((e.keyCode >= 37) && (e.keyCode <= 40)) { haltEvent(e); } }
            if (!xxdialogMode && (xxcurrentView == 13) && (e.keyCode == 116) && (p13filetree != null)) { haltEvent(e); return false; } // F5 Refresh on files
            if (!xxdialogMode && ((xxcurrentView == 15) || (xxcurrentView == 115))) { return agentConsoleHandleKeys(e); }
            if (!xxdialogMode && (xxcurrentView == 4)) {
                if ((e.keyCode === 8) && (userSearchFocus == 0)) { var x = Q('UserSearchInput').value; Q('UserSearchInput').value = (x.substring(0, x.length - 1)); processed = 1; }
                if (e.keyCode === 27) { Q('UserSearchInput').value = ''; processed = 1; }
                if (processed > 0) { if (processed == 1) { mainUpdate(5); } return haltEvent(e); }
            }
            if (xxdialogMode || (xxcurrentView != 1) || (e.ctrlKey == true) || (e.altKey == true) || (e.metaKey == true)) return;
            var processed = 0;
            if (Q('viewselect').value < 4) {
                if ((e.keyCode === 8) && (searchFocus == 0)) { var x = Q('SearchInput').value; Q('KvmSearchInput').value = Q('SearchInput').value = (x.substring(0, x.length - 1)); processed = 1; }
                if (e.keyCode === 27) { Q('KvmSearchInput').value = Q('SearchInput').value = ''; processed = 1; }
                if (processed > 0) { if (processed == 1) { mainUpdate(5); } return haltEvent(e); }
            }
            if (Q('viewselect').value == 4) {
                if ((e.keyCode === 8) && (mapSearchFocus == 0)) { var x = Q('mapSearchLocation').value; Q('mapSearchLocation').value = (x.substring(0, x.length - 1)); processed = 1; }
                if (e.keyCode === 27) { Q('mapSearchLocation').value = ''; mapCloseSearchWindow(); processed = 1; }
            }
        }

        function ondockeyup(e) {
            mobileKbdGotKeypress = false;
            var sk = Q('softKeyboard'); if (sk && document.activeElement === sk) { sk.value = ''; if (typeof _releaseMobileModifiers === 'function') _releaseMobileModifiers(); }
            setSessionActivity();
            if (!xxdialogMode && (xxcurrentView == 11) && desktop && Q('DeskControl').checked) {
                // Check what keys we are allows to send
                if (currentNode != null) {
                    var meshrights = GetNodeRights(currentNode);
                    var inputAllowed = ((features2 & 0x2000) == 0) && ((meshrights == 0xFFFFFFFF) || (((meshrights & 8) != 0) && ((meshrights & 256) == 0)));
                    if (inputAllowed == false) return false;
                    var limitedInputAllowed = ((meshrights != 0xFFFFFFFF) && (((meshrights & 8) != 0) && ((meshrights & 256) == 0) && ((meshrights & 4096) != 0)));
                    if (limitedInputAllowed == true) { if ((e.altKey == true) || (e.ctrlKey == true) || ((e.keyCode < 32) && (e.keyCode != 8) && (e.keyCode != 13)) || (e.keyCode > 90)) return false; }
                }
                return desktop.m.handleKeyUp(e);
            }
            if (!xxdialogMode && (xxcurrentView == 12) && terminal && (terminal.State == 3) && xterm == null) { return terminal.m.TermHandleKeyUp(e); }
            if (!xxdialogMode && (xxcurrentView == 13) && (e.keyCode == 116) && (p13filetree != null)) { p13folderup(9999); haltEvent(e); return false; } // F5 Refresh on files
            if (!xxdialogMode && (xxcurrentView == 4)) { if (((e.keyCode === 8) && (searchFocus == 0)) || e.keyCode === 27) { return haltEvent(e); } }
            if (xxdialogMode && (e.keyCode == 27)) { dialogclose(0); }
            if ((e.shiftKey == true) && (e.keyCode == 27)) { dialogclose(0); Q('KvmSearchInput').value = Q('SearchInput').value = ''; mainUpdate(5); if (desktop != null) { connectDesktop(); } if (terminal != null) { connectTerminal(); } if (files != null) { connectFiles(); } go(1); return; } // Shift-ESC: Reset the web page
            if (xxdialogMode || xxcurrentView != 0 || e.ctrlKey == true || (e.altKey == true) || (e.metaKey == true)) return;
            if (Q('viewselect').value < 4) { if (((e.keyCode === 8) && (searchFocus == 0)) || (e.keyCode === 27)) { return haltEvent(e); } }
            if (Q('viewselect').value == 4) { if (((e.keyCode === 8) && (mapSearchFocus == 0)) || (e.keyCode === 27)) { return haltEvent(e); } }
        }

        //function ondocfocus() { }
        // TODO: Add handleReleaseKeys() for Intel AMT.
        function ondocblur() { if (!xxdialogMode && xxcurrentView == 11 && desktop && Q('DeskControl').checked && desktop.m.handleReleaseKeys) { return desktop.m.handleReleaseKeys(); } }

        // Highlights the device group hovered
        function devGrpMouseHover(element, over) {
            setSessionActivity();
            var view = Q('viewselect').value;
            var e = element.children[1].children[1];
            e.children[0].classList.remove('g1s');
            e.children[1].classList.remove('e2s');
            e.children[2].classList.remove('g2s');
            if (over == 1) {
                e.children[0].classList.add('g1s');
                e.children[1].classList.add('e2s');
                e.children[2].classList.add('g2s');
            }
        }

        // Highlights the device being hovered
        function devMouseHover(element, over) {
            setSessionActivity();
            var view = Q('viewselect').value;
            if (view == 1) {
                var e = element.children[0].children[0].children[1].children[0].children[0].children[0];
                e.children[1].classList.remove('g1s');
                e.children[2].classList.remove('e2s');
                e.children[3].classList.remove('g2s');
                if (over == 1) {
                    e.children[1].classList.add('g1s');
                    e.children[2].classList.add('e2s');
                    e.children[3].classList.add('g2s');
                }
            } else if (view == 2) {
                var e = element;
                e.children[2].classList.remove('g1s');
                e.children[4].classList.remove('e2s');
                e.children[3].classList.remove('g2s');
                if (over == 1) {
                    e.children[2].classList.add('g1s');
                    e.children[4].classList.add('e2s');
                    e.children[3].classList.add('g2s');
                }
            }
        }

        var deviceHeaderId = 0;
        var deviceHeaderTotal = 0;
        var deviceHeadersTitles = {};
        var deviceHeaderCount;
        var deviceHeaders = {};
        var oldviewmode = 0;
        function updateDevices() {
            if ((nodes == null) || (xxcurrentView != 1)) { return; }
            var r = '', c = 0, current = null, count = 0, scount = 0, displayedMeshes = {}, view = Q('viewselect').value, groups = {}, groupCount = {};
            QV('xdevices', view != 4);
            QV('xdevicesmap', view == 4);
            QVH('devListToolbar', view < 3);
            QVH('kvmListToolbar', (view == 3) || (view == 5));
            QVH('devMapToolbar', view == 4);
            QV('devListToolbarSize', (view == 3) || (view == 5));
            QV('devListToolbarSettings', view == 2);
            QV('NoMeshesPanel', (nodes.length == 0) && (meshcount == 0));
            //QV('devListToolbarView', (meshcount != 0) && (nodes.length > 0));
            QV('devListToolbarViewIcons', nodes.length > 0);
            QV('devListToolbarSort', (nodes.length > 0) && (view != 4));
            if (nodes.length == 0) { view = 1; sort = 0; }
            if (view == 4) {
                setTimeout(function () { if (xxmap.map != null) { xxmap.map.updateSize(); } }, 200);
                // TODO
            } else {
                // 3 wide, list view or desktop view
                deviceHeaderId = 0;
                deviceHeaderCount = {};
                deviceHeaderTotal = 0;
                deviceHeaders = {};
                deviceHeadersTitles = {};
                var kvmDivs = [];

                // Perform node sort
                if (sort == 0) { nodes.sort(meshSort); }
                else if (sort == 1) { nodes.sort(powerSort); }
                else if (sort == 2) { if (showRealNames == true) { nodes.sort(deviceHostSort); } else { nodes.sort(deviceSort); } }
                else if (sort == 5) {
                    // If the last seen column is not turned on, turn it on first (we require this to sort the data)
                    if (!(deviceViewSettings && deviceViewSettings.devsCols && deviceViewSettings.devsCols.indexOf('lastseen') >= 0)) {
                        // Request last connection data if not requested yet
                        if (requestedLastConnects == false) { requestedLastConnects = true; meshserver.send({ action: 'lastconnects' }); }
                        // Force initialize the view settings
                        if (deviceViewSettings == null) { deviceViewSettings = {}; }
                        if (!Array.isArray(deviceViewSettings.devsCols)) { deviceViewSettings.devsCols = ['user', 'ip', 'conn', 'lastseen']; }
                        else { deviceViewSettings.devsCols.push('lastseen'); }
                    }
                    nodes.sort(lastConnectSort);
                }
                else if (sort == 6) {
                    // Last Boot Up Time
                    if (!(deviceViewSettings && deviceViewSettings.devsCols && deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0)) {
                        // Force initialize the view settings
                        if (deviceViewSettings == null) { deviceViewSettings = {}; }
                        if (!Array.isArray(deviceViewSettings.devsCols)) { deviceViewSettings.devsCols = ['user', 'ip', 'conn', 'lastbootuptime']; }
                        else { deviceViewSettings.devsCols.push('lastbootuptime'); }
                    }
                    nodes.sort(lastBootUpTimeSort);
                }

                // Compute the width of the device view.
                var totalDeviceViewWidth = Q('column_l').clientWidth - 60;
                var deviceBoxWidth = Math.floor(totalDeviceViewWidth / 301);
                deviceBoxWidth = 301 + Math.floor((totalDeviceViewWidth - (deviceBoxWidth * 301)) / deviceBoxWidth);

                // Compute number of colums
                var colcount = 4;
                if (deviceViewSettings && deviceViewSettings.devsCols) {
                    colcount = deviceViewSettings.devsCols.length + 1;
                    if (deviceViewSettings.devsCols.indexOf('desc') >= 0) { colcount--; } // Description is not an extra column.
                }

                // Go thru the list of nodes and display them
                for (var i in nodes) {
                    var node = nodes[i];
                    if (node.v == false) continue;
                    var mesh2 = meshes[node.meshid];
                    //if ((view == 3) && (mesh2.mtype == 1)) continue;
                    var meshrights = GetNodeRights(node);
                    if (sort == 0) {
                        // Mesh header
                        if (((meshes[node.meshid] ? node.meshid : '*') != current)) {
                            if (((view == 1) || (view == 3) || (view == 5)) && (current != null)) { r += '</div>'; } // Close collapse div
                            deviceHeaderSet();
                            var extra = '';
                            if (view == 2) { r += '<tr><td colspan=' + colcount + '>'; }
                            if (meshes[node.meshid] && (meshes[node.meshid].mtype == 1)) { extra = '<span class=devHeaderx>' + ", solo Intel&reg; AMT" + '</span>'; }
                            if (meshes[node.meshid] && (meshes[node.meshid].mtype == 3)) { if (meshes[node.meshid].relayid) { extra = '<span class=devHeaderx>' + ", Dispositivos retransmitidos" + '</span>'; } else { extra = '<span class=devHeaderx>' + ", Dispositivos locales" + '</span>'; } }
                            if ((view == 1) && (current != null)) { if (c == 2) { r += '<td><div style=width:301px></div></td>'; } if (r != '') { r += '</tr></table>'; } }
                            if (view == 2) { r += '<div>'; }
                            r += '<div class=DevSt style=width:100%;padding-top:4px><span style=float:right>';
                            r += '<span id=DevxHeader' + deviceHeaderId + ' class=devHeaderx></span>' + extra;
                            r += '</span>';
                            if ((view == 1) || (view == 2) || (view == 3) || (view == 5)) {
                                var collapsed = CollapsedGroups[node.meshid];
                                r += '<i role="button" cmenu=expandAllContextMenu onclick=toggleCollapseGroup("' + deviceHeaderId + '","' + node.meshid + '",' + view + ') class="fa-fw fa-solid ' + ((collapsed === true) ? 'fa-chevron-right' : 'fa-chevron-down') + '" id="DevxColImg' + deviceHeaderId + '"></i>';
                            }
                            if (meshes[node.meshid]) {
                                r += '<span id=MxMESH cmenu=meshContextMenu tabindex=0 style=cursor:pointer onclick=gotoMesh("' + node.meshid + '") onkeypress="if (event.key==\'Enter\') gotoMesh(\'' + node.meshid + '\')">' + EscapeHtml(meshes[node.meshid].name) + '</span>' + getMeshActions(mesh2, meshrights) + '</div>';
                                current = node.meshid;
                            } else {
                                r += '<span id=MxMESH><i>' + "Dispositivos Individuales" + '</i></span></div>';
                                current = '*';
                            }
                            if (view == 2) { r += '</div>'; }
                            displayedMeshes[current] = 1;
                            c = 0;
                            if ((view == 1) || (view == 3) || (view == 5)) { r += '<div id=DevxCol' + deviceHeaderId + ((collapsed === true) ? ' style=display:none' : '') + '>'; } // Open collapse div
                        }
                    } else if (sort == 1) {
                        // Power header
                        var pwr = node.pwr ? node.pwr : 0;
                        if (pwr !== current) {
                            if (((view == 1) || (view == 3) || (view == 5)) && (current != null)) { r += '</div>'; } // Close collapse div
                            deviceHeaderSet();
                            if ((view == 1) && (current !== null)) { if (c == 2) { r += '<td><div style=width:301px></div></td>'; } if (r != '') { r += '</tr></table>'; } }

                            if (view == 2) { r += '<tr><td colspan=' + colcount + '>'; }
                            r += '<div class=DevSt style=width:100%;padding-top:4px><span id=DevxHeader' + deviceHeaderId + ' class=devHeaderx style=float:right></span>';
                            if ((view == 1) || (view == 2) || (view == 3) || (view == 5)) {
                                var collapsed = CollapsedGroups['pwr:' + pwr];
                                r += '<i role="button" cmenu=expandAllContextMenu onclick=toggleCollapseGroup("' + deviceHeaderId + '","pwr:' + pwr + '",' + view + ') class="fa-fw fa-solid ' + ((collapsed === true) ? 'fa-chevron-right' : 'fa-chevron-down') + '" id="DevxColImg' + deviceHeaderId + '"></i>'; // Collapse action
                            }
                            r += '<span>' + PowerStateStr2(node.pwr) + '</span></div>';

                            current = pwr;
                            c = 0;
                            if ((view == 1) || (view == 3) || (view == 5)) { r += '<div id=DevxCol' + deviceHeaderId + ((collapsed === true) ? ' style=display:none' : '') + '>'; } // Open collapse div
                        }
                    } else if (sort == 2 || sort == 5 || sort == 6) {
                        // Device header
                        if (current == null) { current = '1'; }
                    }

                    count++;
                    if (view == 1) {
                        // Draw the device standin
                        r += '<div name=xxdevice1 id=xv1' + node._id + ' style=display:inline-block;position:relative;width:' + deviceBoxWidth + 'px;height:50px;padding-top:1px;padding-bottom:1px></div>';
                    } else if (view == 2) {
                        // Draw the device standin
                        var collapseName = node.meshid;
                        if (sort == 1) { collapseName = ('pwr:' + (node.pwr ? node.pwr : 0)); }
                        else if ((sort == 3) || (sort == 4)) { collapseName = 'tag:**xx**xx*TaG*xx**xx**'; }
                        var collapsed = (sort != 3) & (sort != 4) & CollapsedGroups[collapseName];
                        r += '<tr name=xxdevice2 colname=DevxCol' + collapseName + ' style=height:20px' + (collapsed ? ';display:none' : '') + ' id=xv2' + node._id + '></tr>';
                    } else if (((view == 3) || (view == 5)) && (node.conn & 1) && (((meshrights & 8) || (meshrights & 256)) != 0) && (node.agent) && ((node.agent.caps & 1) != 0)) { // Check if we have rights and agent is capable of KVM.
                        // Draw the device (TODO: See if we can replace this with a standin in the future)
                        if ((Object.keys(checkedNodeids).length == 0) || checkedNodeids[node._id]) {
                            r += updateDeviceViewHtml(view, null, node);
                            kvmDivs.push(node._id);
                        }
                    }

                    // If we are displaying devices by tags, put the device in the right tag group.
                    if (((sort == 3) || (sort == 4)) && (r != '')) {
                        if (node.tags) {
                            for (var j in node.tags) {
                                var tag = node.tags[j];
                                if (sort == 4) { if (mesh2) { tag = mesh2.name + ' - ' + node.tags[j]; } else { tag = '**INDV*~*DEVS** - ' + node.tags[j]; } }
                                var collapsed = CollapsedGroups['tag:' + encodeURIComponentEx(tag)];
                                var r2 = r.replace('**xx**xx*TaG*xx**xx**', encodeURIComponentEx(tag) + (collapsed ? ' style=display:none' : ''));
                                if (groups[tag] == null) { groups[tag] = r2; groupCount[tag] = 1; } else { groups[tag] += r2; groupCount[tag] += 1; }
                                if ((view == 3) || (view == 5)) break;
                            }
                        } else {
                            // If sorted by "Groups-Tags" and a device has no tags, put in a group only section.
                            if ((sort == 4) && (mesh2 != null) && ((node.tags == null) || (node.tags.length == 0))) {
                                var tag = mesh2.name;
                                var collapsed = CollapsedGroups['tag:' + encodeURIComponentEx(tag)];
                                var r2 = r.replace('**xx**xx*TaG*xx**xx**', encodeURIComponentEx(tag) + (collapsed ? ' style=display:none' : ''));
                                if (groups[tag] == null) { groups[tag] = r2; groupCount[tag] = 1; } else { groups[tag] += r2; groupCount[tag] += 1; }
                            }
                        }
                        r = '';
                    }

                    deviceHeaderTotal++;
                    if (typeof deviceHeaderCount[node.state] == 'undefined') { deviceHeaderCount[node.state] = 1; } else { deviceHeaderCount[node.state]++; }
                }

                if ((view == 1) && (current != null)) { r += '</div>'; } // Close collapse div

                // Above 32 devices, gray out the auto connect feature.
                if (kvmDivs.length >= 32) { Q('autoConnectDesktopCheckbox').checked = false; }
                QE('autoConnectDesktopCheckbox', kvmDivs.length < 32);

                // If displaying devices by groups, sort the group names and display the devices.
                if ((sort == 3) || (sort == 4)) {
                    var groupNames = [], tagDeviceHeaderId = 0;
                    for (var i in groups) { groupNames.push(i); }
                    //groupNames.sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); });
                    groupNames.sort(function (a, b) { return sortCollator.compare(a.toLowerCase(), b.toLowerCase()); });
                    for (var j in groupNames) {
                        var i = groupNames[j];
                        if (view == 2) {
                            r += '<tr><td colspan=' + colcount + '><div class=DevSt style=width:100%;padding-top:4px>';
                            var collapsed = CollapsedGroups['tag:' + encodeURIComponentEx(i)];
                            r += '<i role="button" cmenu=expandAllContextMenu onclick=toggleCollapseGroup("' + tagDeviceHeaderId + '","tag:' + encodeURIComponentEx(i) + '",2) class="fa-fw fa-solid ' + ((collapsed === true) ? 'fa-chevron-right' : 'fa-chevron-down') + '" id="DevxColImg' + tagDeviceHeaderId + '"></i>'; // Collapse action
                            r += '<span class=devHeaderx style=float:right>' + groupCount[i] + ' node' + ((groupCount[i] > 1) ? 's' : '') + '</span><span>' + EscapeHtml(i).split('|').join(' &rarr; ').split('**INDV*~*DEVS**').join('<i>' + "Dispositivos Individuales" + '</i>') + '</span></div>' + groups[i];
                        } else {
                            r += '<div class=DevSt style=width:100%;padding-top:4px><span class=devHeaderx style=float:right>' + format(((groupCount[i] > 1) ? '{0} nodes' : '{0} node'), groupCount[i]) + '</span>';
                            var collapsed = CollapsedGroups['tag:' + encodeURIComponentEx(i)];
                            r += '<i role="button" cmenu=expandAllContextMenu onclick=toggleCollapseGroup("' + tagDeviceHeaderId + '","tag:' + encodeURIComponentEx(i) + '") class="fa-fw fa-solid ' + ((collapsed === true) ? 'fa-chevron-right' : 'fa-chevron-down') + '" id="DevxColImg' + tagDeviceHeaderId + '"></i>'; // Collapse action
                            r += '<span>' + EscapeHtml(i).split('|').join(' &rarr; ').split('**INDV*~*DEVS**').join('<i>' + "Dispositivos Individuales" + '</i>') + '</span></div>';
                            r += '<div id=DevxCol' + tagDeviceHeaderId + ((collapsed === true) ? ' style=display:none' : '') + '>' + groups[i] + '</div>'; // Open collapse div
                        }
                        tagDeviceHeaderId++;
                    }
                }

                // If there is nothing to display, explain the problem
                var viewNothing = false;
                if ((r == '') && (nodes.length > 0) && ((Q('SearchInput').value != '') || (Q('DevFilterSelect').value != 0))) {
                    viewNothing = true;
                    if (sort == 3) {
                        r = '<div style="margin:30px">' + "No se incluyen dispositivos en ningún grupo, haga clic en un \"Grupo\" de dispositivos para agregarlos a un grupo." + '</div>';
                    } else {
                        r = '<div style="margin:30px">' + "No hay dispositivos que coincidan con esta búsqueda." + ' <a onclick=clearDeviceSearch()>' + "Borrar filtro de búsqueda" + '</a></div>';
                    }
                }

                if ((view == 1) && (c == 2)) r += '<td><div style=width:301px></div></td>'; // Adds device padding

                // Display all empty device groups, we need to do this because users can add devices to these at any time.
                if ((sort == 0) && ((Q('SearchInput').value == '') && (Q('DevFilterSelect').value == 0)) && (view < 3)) {
                    var deviceHeaderId2 = deviceHeaderId, sortedMeshes = [];
                    for (var i in meshes) { sortedMeshes.push(meshes[i]); }
                    sortedMeshes.sort(nameSort);
                    for (var i in sortedMeshes) {
                        var mesh = sortedMeshes[i], meshrights = GetMeshRights(mesh);
                        if (displayedMeshes[mesh._id] == null) {
                            if ((current != '') && (r != '')) { r += '</tr></table>'; }
                            r += '<table style=width:100%;padding-top:4px cellpadding=0 cellspacing=0><tr><td colspan=3 class=DevSt>';

                            // Collapsing header & start collapsing area
                            deviceHeaderId2++;
                            var collapsed = CollapsedGroups[mesh._id];
                            r += '<i role="button" cmenu=expandAllContextMenu onclick=toggleCollapseGroup("' + deviceHeaderId2 + '","' + mesh._id + '") class="fa-fw fa-solid ' + ((collapsed === true) ? 'fa-chevron-right' : 'fa-chevron-down') + '" id="DevxColImg' + deviceHeaderId2 + '"></i>'; // Collapse action

                            r += '<span id=MxMESH style=cursor:pointer onclick=gotoMesh("' + mesh._id + '")>' + EscapeHtml(mesh.name) + '</span><span>';
                            r += getMeshActions(mesh, meshrights);
                            r += '</span></td></tr><tr>';
                            if (mesh.mtype == 1) {
                                r += '<td><div style=padding:10px><i>' + "No hay dispositivos Intel&reg; AMT en este grupo de dispositivos";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "agregar uno" + '</a>'; }
                            } else if (mesh.mtype == 2) {
                                r += '<td><div id=DevxCol' + deviceHeaderId2 + ((collapsed === true) ? ' style=display:none' : '') + '>'; // Open collapse div
                                r += '<div style=padding:10px><i>' + "No hay dispositivos en este grupo de dispositivos";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "agregar uno" + '</a>'; }
                            } else if (mesh.mtype == 3) {
                                r += '<td><div id=DevxCol' + deviceHeaderId2 + ((collapsed === true) ? ' style=display:none' : '') + '>'; // Open collapse div
                                r += '<div style=padding:10px><i>' + "No hay dispositivos locales en este grupo de dispositivos";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "agregar uno" + '</a>'; }
                            } else if (mesh.mtype == 4) {
                                r += '<td><div id=DevxCol' + deviceHeaderId2 + ((collapsed === true) ? ' style=display:none' : '') + '>'; // Open collapse div
                                r += '<div style=padding:10px><i>' + "No hay dispositivos en este grupo de dispositivos";
                            }
                            r += '.</i></div></td>';
                            r += '</div>'; // End collapsing area

                            current = mesh._id;
                            count++;
                        }
                    }
                }

                if ((r != '') && (viewNothing == false)) {
                    r += '</table>';
                    if (view == 2) {
                        var colums = '';

                        // Use defaults if needed
                        if (deviceViewSettings == null) { deviceViewSettings = {}; }
                        if (!Array.isArray(deviceViewSettings.devsCols)) { deviceViewSettings.devsCols = ['user', 'ip', 'conn']; }

                        // Display configured columns
                        if (deviceViewSettings.devsCols.indexOf('agtype') >= 0) { colums += '<th style=width:100px>' + "Tipo de agente"; }
                        if (deviceViewSettings.devsCols.indexOf('agver') >= 0) { colums += '<th style=width:100px>' + "Versión del agente"; }
                        if (deviceViewSettings.devsCols.indexOf('os') >= 0) { colums += '<th style=width:160px>' + "SO"; }
                        if (deviceViewSettings.devsCols.indexOf('tags') >= 0) { colums += '<th style=width:120px>' + "Etiquetas"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsav') >= 0) { colums += '<th style=width:100px>' + "Windows AV"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) { colums += '<th style=width:120px>' + "Windows Update"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) { colums += '<th style=width:120px>' + "Windows Firewall"; }
                        if (deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) { colums += '<th style=width:120px>' + "Last Boot Up Time"; }
                        if (deviceViewSettings.devsCols.indexOf('idletime') >= 0) { colums += '<th style=width:100px title="' + "Actualizado cada 5 minutos" + '">' + "Tiempo de inactividad"; }
                        if (deviceViewSettings.devsCols.indexOf('links') >= 0) { colums += '<th style=width:120px>' + "Enlaces"; }
                        if(features3 & 0x00000002){
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:180px>' + "Usuario"; }
                        } else {
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:120px>' + "Usuario"; }
                        }
                        if (deviceViewSettings.devsCols.indexOf('ip') >= 0) { colums += '<th style=width:120px>' + "Dirección"; }
                        if (deviceViewSettings.devsCols.indexOf('conn') >= 0) { colums += '<th style=width:100px>' + "Conectividad"; }
                        if (deviceViewSettings.devsCols.indexOf('amthost') >= 0) { colums += '<th style=width:100px>' + "AMT Host"; }
                        if (deviceViewSettings.devsCols.indexOf('amtstate') >= 0) { colums += '<th style=width:100px>' + "AMT State"; }
                        if (deviceViewSettings.devsCols.indexOf('lastseen') >= 0) {
                            colums += '<th style=width:120px>' + "Ultima vez visto";
                            if (requestedLastConnects == false) { requestedLastConnects = true; meshserver.send({ action: 'lastconnects' }); }
                        }
                        // This height of 1 div at the end to fix a problem in Linux firefox browsers
                        r = '<table style=width:100%;margin-top:4px;table-layout:fixed cellpadding=0 cellspacing=0><th>' + colums + r + '</tr></table><div style=height:1px></div>';
                    }
                } else {
                    r += '</table>';
                    if (sort == 3) { r = '<div style="margin:10px"><i>' + "No se encontraron dispositivos con etiquetas." + '</i></div>'; }
                }

                // Add a "Add Device Group" option
                r += '<div style=border-top-style:solid;border-top-width:1px;border-top-color:#DDDDDD;cursor:pointer;font-size:small;margin-top:4px>';
                if ((view < 3) && (sort == 0) && (Object.keys(meshes).length > 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 64) == 0))) {
                    r += '<a href=# onclick="return account_createMesh()" title="' + "Crear un nuevo grupo de dispositivos." + '" style=cursor:pointer>' + "Agregar Grupo de Dispositivos" + '</a>&nbsp';
                }
                if ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 128) == 0)) {
                    r += '<a href=# onclick=\'return p10showMeshCmdDialog(0)\' style=cursor:pointer title="' + "Descargar MeshCmd, una herramienta de línea de comandos que realiza muchas funciones." + '">' + "MeshCmd" + '</a>&nbsp';
                    if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) { r += '<a href=# onclick=\'return p10showMeshRouterDialog()\' style=cursor:pointer title="' + "Descargar MeshCentral Router, una herramienta de mapeo de puertos TCP." + '">' + "Router" + '</a>&nbsp'; }
                }
                r += '</div><div style=height:60px></div>';

                QH('xdevices', r);
                deviceHeaderSet();

                for (var i in deviceHeaders) { QH(i, deviceHeaders[i]); }
                for (var i in deviceHeadersTitles) { Q(i).title = deviceHeadersTitles[i]; }
                p1updateInfo();

                // Take care of KVM surfaces in desktop view mode
                if (((view == 3) || (view == 5)) && (xxcurrentView == 1)) {

                    // Figure out and adjust the size to fill the width of the div
                    var vsize = [{ x: 180, y: 101 }, { x: 302, y: 169 }, { x: 454, y: 255 }][Q('sizeselect').selectedIndex];
                    if ((view == 3)) { // If we are in fixed width mode, correct the size for the screen width.
                        var realw = vsize.x + 2, tw = totalDeviceViewWidth - 5, xw = Math.floor(tw / realw);
                        xw = realw + Math.floor((tw - (xw * realw)) / xw);
                        vsize.y = vsize.y * (xw / vsize.x);
                        vsize.x = xw;
                    }

                    for (var i in multiDesktop) { multiDesktop[i].xxdelete = true; }
                    for (var i in kvmDivs) {
                        var id = kvmDivs[i], shortid = id.split('/')[2], desk = multiDesktop[id];
                        if (desk != null) {
                            // This device already has a canvas, use it.
                            var width = (view == 5) ? ((desk.m.width * vsize.y) / desk.m.height) : vsize.x;
                            desk.m.CanvasId.setAttribute('style', 'background-color:black;width:' + width + 'px;height:' + vsize.y + 'px');
                            Q('xkvmid_' + shortid).appendChild(desk.m.CanvasId);
                            delete desk.xxdelete;
                            QH('skvmid_' + shortid, ["Desconectado", "Conectando....", "Configurar...", '', ''][((desk.m.State == null) ? desk.m.state : desk.m.State)]);
                        } else {
                            var node = getNodeFromId(id);
                            if ((desktopNode == node) && (desktop != null)) { // Check if the main desktop is this device, if it is, use that.
                                // This device already has a canvas, use it.
                                var c = desktop.m.CanvasId;
                                var width = (view == 5) ? ((desktop.m.width * vsize.y) / desktop.m.height) : vsize.x;
                                c.setAttribute('id', 'kvmid_' + shortid);
                                c.setAttribute('style', 'background-color:black;width:' + width + 'px;height:' + vsize.y + 'px');
                                c.setAttribute('onclick', 'toggleKvmDevice(\'' + id + '\')');
                                c.removeAttribute('onmousedown');
                                c.removeAttribute('onmouseup');
                                c.removeAttribute('onmousemove');
                                Q('xkvmid_' + shortid).appendChild(c);
                                QH('skvmid_' + shortid, ["Desconectado", "Conectando....", "Configurar...", '', ''][((desktop.m.State == null) ? desktop.m.state : desktop.m.State)]);
                                if (desktop.m.SendCompressionLevel) { desktop.m.SendCompressionLevel(multidesktopsettings.agentencoding, multidesktopsettings.quality, multidesktopsettings.scaling, multidesktopsettings.framerate); }
                                desktop.shortid = shortid;
                                desktop.onStateChanged = onMultiDesktopStateChange;
                                desktop.m.onRemoteInputLockChanged = null;
                                desktop.m.onKeyboardStateChanged = null;
                                multiDesktop[id] = desktop;
                                desktop = desktopNode = currentNode = null;
                                // Setup a replacement desktop
                                QH('DeskParent', '<canvas id="Desk" oncontextmenu="return false" onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event)></canvas>');
                            } else {
                                // This is a new device, create a canvas for it.
                                var c = document.createElement('canvas');
                                c.setAttribute('id', 'kvmid_' + shortid);
                                c.setAttribute('width', 640);
                                c.setAttribute('height', 480);
                                c.setAttribute('oncontextmenu', 'return false');
                                c.setAttribute('style', 'background-color:black;width:' + vsize.x + 'px;height:' + vsize.y + 'px');
                                c.setAttribute('onclick', 'toggleKvmDevice(\'' + id + '\')');
                                try { Q('xkvmid_' + shortid).appendChild(c); } catch (ex) { }
                                // Check if we need to auto-connect
                                if (Q('autoConnectDesktopCheckbox').checked == true) { setTimeout(function () { connectMultiDesktop(node, 1); }, 100); }
                            }
                        }
                    }
                    for (var i in multiDesktop) {
                        // If a device is no longer viewed, disconnect it.
                        if (multiDesktop[i].xxdelete == true) { multiDesktop[i].Stop(); delete multiDesktop[i]; }
                        else if (debugmode && multiDesktop[i].m && multiDesktop[i].m.onScreenSizeChange) {
                            mdeskAdjust(multiDesktop[i].m, multiDesktop[i].m.ScreenWidth, multiDesktop[i].m.ScreenHeight, multiDesktop[i].m.CanvasId); // Adjust screen size change
                        }
                    }
                    deskAdjust();
                } else {
                    disconnectAllKvmFunction();
                    Q('autoConnectDesktopCheckbox').checked = false;
                }
            }
            oldviewmode = view;
            onDevicesScrollEx();
        }


        var onDevicesTouchActive = false;
        var onDevicesScrollnagleTimer = null;
        function onDevicesScroll(top) {
            if (typeof top == 'undefined') top = false;
            if (top) Q('xdevices').scrollTop = 0;
            if ((onDevicesScrollnagleTimer != null) || (onDevicesTouchActive)) return;
            onDevicesScrollnagleTimer = setTimeout(onDevicesScrollEx, 250);
        }

        function onDeviceTouch(x) {
            if (onDevicesTouchActive == x) return;
            onDevicesTouchActive = x;
            if (x == false) onDevicesScrollEx();
        }

        function onDevicesScrollEx() {
            if (onDevicesScrollnagleTimer != null) { clearTimeout(onDevicesScrollnagleTimer); onDevicesScrollnagleTimer = null; }
            var visibleTop = Q('xdevices').scrollTop - 200, visibleBottom = Q('xdevices').scrollTop + Q('xdevices').clientHeight + 200;
            var view = Q('viewselect').value, devdivs = document.getElementsByName('xxdevice' + view);
            for (var i = 0; i < devdivs.length; i++) {
                if ((devdivs[i].offsetTop >= visibleTop) && (devdivs[i].offsetTop < visibleBottom)) {
                    var node = getNodeFromId(devdivs[i].id.substring(3));
                    if (node != null) { updateDeviceViewHtml(view, devdivs[i], node); } else { devdivs[i].innerHTML = ''; }
                } else {
                    devdivs[i].innerHTML = '';
                }
            }
        }

        // Update a single device in the current view
        function updateDeviceViewDevice(node) {
            var view = Q('viewselect').value;
            if ((view != 1) && (view != 2)) { mainUpdate(4); return; }
            if (typeof node == 'string') { node = getNodeFromId(node); }
            if (node == null) return;
            var devdiv = Q('xv' + view + node._id);
            if ((devdiv != null) && (devdiv.innerHTML != '')) { updateDeviceViewHtml(view, devdiv, node); } // Only update if the device is visible
        }

        function updateDeviceViewHtml(view, div, node) {
            var title = EscapeHtml(node.name);
            if (title.length == 0) { title = '<i>' + "Ninguno" + '</i>'; }
            if ((node.rname != null) && (node.rname.length > 0)) { title += ' / ' + EscapeHtml(node.rname); }
            var name = EscapeHtml(node.name);
            if (showRealNames == true && node.rname != null) name = EscapeHtml(node.rname);
            if (name.length == 0) { name = '<i>' + "Ninguno" + '</i>'; }
            if (((view == 1) && (div.parentNode.style['display'] == 'none')) || ((view == 2) && (div.style['display'] == 'none'))) { div.innerHTML = ''; return; } // If this section is collapsed, don't render

            // Add device notification icons
            var devNotify = '', devNotifySub = '';

            // This device is "starred"
            if (stars[node._id] == 1) {
                if (view == 2) {
                    devNotifySub += '<img class=deviceNotifySmallDotSub src=images/icon-star-notify-10.png width=10 height=10>';
                } else {
                    devNotifySub += '<img class=deviceNotifyDotSub src=images/icon-star-notify-16.png width=16 height=16>';
                }
            }

            // This device has session information
            if (node.sessions != null) {
                // Display any agent messages
                if (node.sessions.msg != null) {
                    if (view == 2) {
                        devNotifySub += '<div onclick=showDeviceMessages(\'' + node._id + '\',null,event) style="width:10;height:10" class=deviceNotifySmallDotSub></div>';
                    } else {
                        devNotifySub += '<div onclick=showDeviceMessages(\'' + node._id + '\',null,event) style="width:16;height:16" class=deviceNotifyDotSub>' + Object.keys(node.sessions.msg).length + '</div>';
                    }
                }

                // Sessions are active
                if ((node.sessions.kvm != null) || (node.sessions.terminal != null) || (node.sessions.files != null) || (node.sessions.tcp != null) || (node.sessions.udp != null)) {
                    if (view == 2) {
                        devNotifySub += '<img onclick=showDeviceSessions(\'' + node._id + '\',null,event) class=deviceNotifySmallDotSub src=images/icon-relay-notify10.png width=10 height=10>';
                    } else {
                        devNotifySub += '<img onclick=showDeviceSessions(\'' + node._id + '\',null,event) class=deviceNotifyDotSub src=images/icon-relay-notify.png width=16 height=16>';
                    }
                }

                // Help is required
                if (node.sessions.help != null) {
                    if (view == 2) {
                        devNotifySub += '<img onclick=showDeviceHelpRequests(\'' + node._id + '\',null,event) class=deviceNotifySmallDotSub src=images/icon-help-notify-10.png width=10 height=10>';
                    } else {
                        devNotifySub += '<img onclick=showDeviceHelpRequests(\'' + node._id + '\',null,event) class=deviceNotifyDotSub src=images/icon-help-notify-16.png width=16 height=16>';
                    }
                }

                // Battery state
                if ((node.sessions.battery != null) && (view == 1)) {
                    var bat = node.sessions.battery;
                    var statestr = '';
                    if (bat.state == 'ac') { statestr = "El dispositivo está enchufado"; }
                    if (bat.state == 'dc') { statestr = "El dispositivo funciona con batería"; }

                    var levelstr = '', levelnum = -1;
                    if ((typeof bat.level == 'number') && (bat.level >= 0) && (bat.level <= 100)) {
                        levelstr = bat.level + '%';
                        levelnum = (Math.floor((bat.level + 10) / 25) + 1);
                        if (levelnum > 5) { lvl = 5; }
                        if (bat.state == 'ac') { if (bat.level == 100) { levelnum = 11; } else { levelnum += 5; } }
                    }

                    if (levelnum > 0) {
                        devNotify += '<div class="deviceBatterySmall deviceBatterySmall' + levelnum + '" title="' + ((statestr != null) ? (statestr + ', ' + levelstr) : levelstr) + '"></div>';
                    }
                }
            }

            // Add any device icons
            if (devNotifySub != '') {
                if (view == 2) {
                    devNotify += '<div class=deviceNotifySmallDot>' + devNotifySub + '</div>';
                } else {
                    devNotify += '<div class=deviceNotifyDot>' + devNotifySub + '</div>';
                }
            }

            // Node
            var icon = node.icon;
            if (((!node.conn) || (node.conn == 0)) && (node.mtype != 3)) { icon += ' gray'; }

            if (view == 1) {
                div.innerHTML = '<table id=devs cmenu=devsContentMenu onmouseover=devMouseHover(this,1) onmouseout=devMouseHover(this,0) style=width:100%;height:100%><tr><td style=width:22px><input class="' + node.meshid + ' DeviceCheckbox form-check-input me-2" onchange=p1devcheck(event) value=devid_' + node._id + ' type=checkbox ' + (checkedNodeids[node._id] ? ' checked' : '') + '></td><td><table onmouseup=gotoDevice(\'' + node._id + '\',null,null,event) border=0 cellspacing=0 style=width:100%;height:100%;cursor:pointer><tr><td style=width:50px tabindex=0 onkeypress="if (event.key==\'Enter\') gotoDevice(\'' + node._id + '\',null,null,event)"><div class="i' + icon + '" style=width:50px></div></td><td class=g1t></td><td class=e2t><div class=e1t style=width:' + ((div.clientWidth) - 120) + 'px title="' + title + '">' + name + '</div><div>' + NodeStateStr(node) + '</div></td><td class=g2t></td></tr></table></td></tr></table>' + devNotify;
            } else if (view == 2) {
                var states = [];
                if (node.conn) {
                    if ((node.conn & 1) != 0) {
                        if (node.mtype == 4) {
                            if (node.porttype == 'PDU') {
                                states.push('<span title="' + "El puerto del conmutador está listo para usarse." + '">' + "Cambiar" + '</span>');
                            } else {
                                states.push('<span title="' + "El puerto IP-KVM está conectado y listo para su uso." + '">' + "IP-KVM" + '</span>');
                            }
                        } else {
                            states.push('<span title="' + "El Agente Mesh está conectado y listo para su uso." + '">' + "Agente" + '</span>');
                        }
                    }
                    if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA está conectado y listo para su uso." + '">' + "CIRA" + '</span>'); }
                    else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT es enrutable." + '">' + "AMT" + '</span>'); }
                    if ((node.conn & 8) != 0) { states.push('<span title="' + "Se puede alcanzar al Agente Mesh utilizando otro agente en modo Relay." + '">' + "Relé" + '</span>'); }
                    if ((node.conn & 16) != 0) { states.push('<span title="' + "La conexión MQTT al dispositivo está activa." + '">' + "MQTT" + '</span>'); }
                }
                if (node.mtype == 3) {
                    var mesh = meshes[node.meshid];
                    if (mesh && mesh.relayid) {
                        states.push('<span title="' + "Conexión de red local a través de un agente de retransmisión." + '">' + "Relé" + '</span>');
                    } else {
                        states.push('<span title="' + "Conexión de red local." + '">' + "Local" + '</span>');
                    }
                }

                if (node.desc && (deviceViewSettings.devsCols.indexOf('desc') >= 0)) { name = '<div class="flex-grow-1">' + name + '</div><div style=float:right' + (stars[node._id] == 1 ? ';padding-right:15px' : '') + '>' + EscapeHtml(node.desc) + '</div>'; }

                var collapseName = node.meshid;
                if (sort == 1) { collapseName = ('pwr:' + (node.pwr ? node.pwr : 0)); }
                else if ((sort == 3) || (sort == 4)) { collapseName = 'tag:**xx**xx*TaG*xx**xx**'; }
                var collapsed = (sort != 3) & (sort != 4) & CollapsedGroups[collapseName];
                var r = '<td style=position:relative><div id=devs cmenu=devsContentMenu class=bar18 tabindex=0 onmouseover=devMouseHover(this,1) onmouseout=devMouseHover(this,0) style=height:18px;width:100%;font-size:medium onkeypress="if (event.key==\'Enter\') gotoDevice(\'' + node._id + '\',null,null,event)">';
                r += '<div class=deviceBarCheckbox><input class="' + node.meshid + ' DeviceCheckbox form-check-input me-2" value=devid_' + node._id + ' type=checkbox onchange=p1devcheck(event) ' + (checkedNodeids[node._id] ? ' checked' : '') + '></div>';
                r += '<div class=deviceBarIcon onmouseup=gotoDevice(\'' + node._id + '\',null,null,event)><div class="j' + icon + '" style=width:16px;margin-top:1px;margin-left:2px;height:16px></div></div>';
                r += '<div class=g1 style=height:18px;float:left></div><div class=g2 style=height:18px;float:right></div>';
                r += '<div class="style10 d-flex align-items-center" style=cursor:pointer;font-size:14px;max-height:18px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap title="' + title + '" onmouseup=gotoDevice(\'' + node._id + '\',null,null,event)>' + name + '</div></div>' + devNotify + '</td>';

                // Use defaults if needed
                if (deviceViewSettings == null) { deviceViewSettings = {}; }
                if (!Array.isArray(deviceViewSettings.devsCols)) { deviceViewSettings.devsCols = ['user', 'ip', 'conn']; }

                // Display configured columns
                if (deviceViewSettings.devsCols.indexOf('agtype') >= 0) { r += '<td style=text-align:center;font-size:x-small;padding-left:4px;padding-right:4px title="' + EscapeHtml(((node.mtype != 3) && node.agent && node.agent.id && (node.agent.id <= agentsStr.length)) ? agentsStr[node.agent.id] : '') + '">' + EscapeHtml(((node.mtype != 3) && node.agent && node.agent.id && (node.agent.id <= agentsStr.length)) ? agentsStr[node.agent.id] : '').replace(',', '<br />'); } // Agent type
                if (deviceViewSettings.devsCols.indexOf('agver') >= 0) { r += '<td style=text-align:center;font-size:x-small;padding-left:4px;padding-right:4px title="' + EscapeHtml((node.agent && node.agent.core) ? node.agent.core : '') + '">' + EscapeHtml((node.agent && node.agent.core) ? node.agent.core : '').replace(',', '<br />'); } // Agent core
                if (deviceViewSettings.devsCols.indexOf('os') >= 0) { // Operating System
                    if (node.mtype == 3) {
                        var osstr = ''; if (node.agent.id == 4) { osstr = 'Windows'; } if (node.agent.id == 6) { osstr = 'Linux'; } if (node.agent.id == 29) { osstr = 'MacOS'; }
                        r += '<td style=text-align:center;font-size:x-small title="' + EscapeHtml(osstr) + '">' + EscapeHtml(osstr);
                    } else {
                        r += '<td style=text-align:center;font-size:x-small title="' + EscapeHtml(node.osdesc ? node.osdesc : '') + '">' + EscapeHtml(node.osdesc ? node.osdesc : '');
                    }
                }
                if (deviceViewSettings.devsCols.indexOf('tags') >= 0) { // Tags
                    r += '<td style=text-align:center;font-size:x-small>';
                    var groupingTags = '';
                    if (node.tags != null) {
                        for (var i in node.tags) {
                            groupingTags += '<span class=tagSpan>' + EscapeHtml(node.tags[i]) + '</span> ';
                        }
                    }
                    r += '<span style=line-height:20px>' + groupingTags + '</span>';
                }
                if (deviceViewSettings.devsCols.indexOf('windowsav') >= 0) { // Windows AV
                    r += '<td style=text-align:center>' + ((node.wsc && node.wsc.antiVirus != null) ? (node.wsc.antiVirus == 'OK' ? '<span style=color:green>' + "OK" + '</span>' : '<span style=color:red>' + "MALO" + '</span>') : "");
                }
                if (deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) {// Windows Update
                    r += '<td style=text-align:center>' + ((node.wsc && node.wsc.autoUpdate != null) ? (node.wsc.autoUpdate == 'OK' ? '<span style=color:green>' + "OK" + '</span>' : '<span style=color:red>' + "MALO" + '</span>') : "");
                }
                if (deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) { // Windows Firewall
                    r += '<td style=text-align:center>' + ((node.wsc && node.wsc.firewall != null) ? (node.wsc.firewall == 'OK' ? '<span style=color:green>' + "OK" + '</span>' : '<span style=color:red>' + "MALO" + '</span>') : "");
                }
                if (deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) { // Last Boot Up Time
                    r += '<td style=text-align:center;font-size:x-small>' + ((node.lastbootuptime != null) ? printDateTime(new Date(node.lastbootuptime)) : "");
                }
                if (deviceViewSettings.devsCols.indexOf('idletime') >= 0) { // Idle Time
                    r += '<td style=text-align:center;font-size:x-small>' + ((node.idletime != null && node.idletime != -1) ? printTimer(node.idletime) : "");
                }
                if (deviceViewSettings.devsCols.indexOf('links') >= 0) { r += '<td style=text-align:center;font-size:x-small>' + getShortRouterLinks(node); } // Links
                if (deviceViewSettings.devsCols.indexOf('user') >= 0) { r += '<td style=text-align:center>' + getUserShortStr(node); } // User
                if (deviceViewSettings.devsCols.indexOf('ip') >= 0) { var ip = ''; if (node.mtype == 3) { ip = node.host; } else if (node.ip) { ip = node.ip; } r += '<td style=text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis title="' + EscapeHtml(ip) + '">' + EscapeHtml(ip); } // IP address
                if (deviceViewSettings.devsCols.indexOf('conn') >= 0) { r += '<td style=text-align:center>' + states.join('&nbsp;+&nbsp;'); } // Connectivity
                if (deviceViewSettings.devsCols.indexOf('amthost') >= 0) { r += '<td style=text-align:center>' + (((node.intelamt == null) || (node.intelamt.host == null)) ? '' : EscapeHtml(node.intelamt.host)); }
                if (deviceViewSettings.devsCols.indexOf('amtstate') >= 0) {
                    var amtstate = '';
                    if (node.intelamt) {
                        if (node.intelamt.state == 0) { amtstate = ' <span title="' + "Intel&reg; AMT is not activated" + '">' + "Pre" + '</span>'; }
                        if (node.intelamt.state == 1) { amtstate = ' <span title="' + "Intel&reg; AMT is in activation mode" + '">' + "In" + '</span>'; }
                        if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { amtstate = ' <span title="' + "Intel&reg; AMT está activado en Modo de Control de Cliente (CCM)" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { amtstate += ' <span title="' + "Intel&reg; AMT esta activado en Admin Control Mode (ACM)" + '">' + "ACM" + '</span>'; } }
                    }
                    r += '<td style=text-align:center>' + amtstate;
                }
                if (deviceViewSettings.devsCols.indexOf('lastseen') >= 0) { r += '<td style=text-align:center;font-size:x-small>'; if (node.conn > 0) { r += "Conectado"; } else if (node.lastconnect != null) { r += printDateTime(new Date(node.lastconnect)); } }

                div.innerHTML = r;
            } else if ((view == 3) || (view == 5)) {
                // Draw the device and canvas
                var r = '<div id=devs cmenu=devsContentMenu style=display:inline-block;position:relative;margin:1px;background-color:lightgray;border-radius:5px;position:relative><div tabindex=0 style=padding:3px;cursor:pointer onmouseup=gotoDevice(\'' + node._id + '\',11,null,event) onkeypress="if (event.key==\'Enter\') gotoDevice(\'' + node._id + '\',11,null,event)">' + devNotify;
                //r += '<input class="' + node.meshid + ' DeviceCheckbox" onclick=p1updateInfo() value=devid_' + node._id + ' type=checkbox style=float:left>';
                r += '<div class="j' + icon + '" style=width:16px;float:left></div>&nbsp;' + name + '</div>';
                r += '<span onmouseup=gotoDevice(\'' + node._id + '\',null,null,event)></span><div id=xkvmid_' + node._id.split('/')[2] + '><div id=skvmid_' + node._id.split('/')[2] + ' tabindex=0 style="position:absolute;color:white;left:5px;top:27px;text-shadow:0px 0px 5px #000;z-index:10;cursor:default" onclick=toggleKvmDevice(\'' + node._id + '\') onkeypress="if (event.key==\'Enter\') toggleKvmDevice(\'' + node._id + '\')">' + "Desconectado" + '</div></div>';
                r += '</div>';
                return r;
            }
        }

        // Return HTML with a list of MeshCentral Router links
        function getShortRouterLinks(node) {
            var x = '', meshrights = GetNodeRights(node);

            // RDP link, show this link only of the remote machine is Windows.
            if ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && (node.agent.id != 14)) {
                if (webRelayPort != 0) {
                    x += '<a href=# onclick=p10WebRouter("' + node._id + '",1,' + (node.httpport ? node.httpport : 80) + ')>' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + '</a>&nbsp;';
                    x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,' + (node.httpsport ? node.httpsport : 443) + ')>' + "HTTPS" + ((node.httspport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + '</a>&nbsp;';
                }
                if ((node.agent.id > 0) && (node.agent.id < 5)) {
                    if (navigator.platform.toLowerCase() == 'win32') {
                        if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
                            x += '<a href=# cmenu=altPortContextMenu id=rdpMCRouterLink onclick=p10MCRouter("' + node._id + '",3)>' + "RDP" + '</a>&nbsp;';
                        }
                    }
                }
                if (node.agent.id > 4) {
                    if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                        if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
                            x += '<a href=# onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ')>' + "SSH" + '</a>&nbsp;';
                        }
                    }
                    if (navigator.platform.toLowerCase() == 'win32') {
                        if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
                            x += '<a href=# onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ')>' + "SCP" + '</a>&nbsp;';
                        }
                    }
                }
                if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                    if ((serverinfo.devicemeshrouterlinks != null) && (Array.isArray(serverinfo.devicemeshrouterlinks.extralinks))) {
                        for (var i in serverinfo.devicemeshrouterlinks.extralinks) {
                            var r = serverinfo.devicemeshrouterlinks.extralinks[i], p = '\"' + r.protocol + '\"';
                            if (doesDeviceMatchFilterTags(node, r.filter)) {
                                if ((node.mtype == 3) && ((r.protocol == 'mcrdesktop') || (r.protocol == 'mcrfiles'))) continue;
                                if (typeof r.protocol == 'number') { p = r.protocol; } else if (r.protocol == 'http') { p = 1; } else if (r.protocol == 'https') { p = 2; } else if (r.protocol == 'rdp') { p = 3; } else if (r.protocol == 'ssh') { p = 4; } else if (r.protocol == 'scp') { p = 5; } else if (r.protocol == 'mcrdesktop') { p = 6; } else if (r.protocol == 'mcrfiles') { p = 7; }
                                x += '<a href=# onclick=p10MCRouter("' + node._id + '",' + p + ',' + r.port + (r.ip ? (',\"' + r.ip + '\"') : ',null') + ',' + (r.localport ? r.localport : 0) + ')>' + r.name + '</a>&nbsp;';
                            }
                        }
                    }
                }
            }

            return x;
        }

        // Check if this device matches any of the given filters
        function doesDeviceMatchFilterTags(node, filter) {
            if (filter == null) return true; // No filters, every device matches.
            if (!Array.isArray(filter)) return false; // Bad filter
            if (filter.indexOf(node.meshid) >= 0) return true; // Device group match
            if (filter.indexOf(node._id) >= 0) return true; // Nodeid match
            if (Array.isArray(node.tags)) { for (var i in node.tags) { if (filter.indexOf('tag:' + node.tags[i]) >= 0) return true; } } // Tag match
            return false;
        }

        // Show device help requests
        function showDeviceHelpRequests(nodeid, force, e) {
            if (e) haltEvent(e);
            if (xxdialogMode && !force) return false;
            var node = null, x = '';
            if (nodeid == null) { node = currentNode; } else { node = getNodeFromId(nodeid); }
            if ((node == null) || (node.sessions == null)) { setDialogMode(0); return false; }
            if (node.sessions.help != null) { for (var j in node.sessions.help) { x += '<div style="background-color:#CCC;padding:6px;border-radius:6px"><a style="float:right" onclick=closeDeviceHelpRequest("' + encodeURIComponentEx(j) + '","' + encodeURIComponentEx(node._id) + '")>' + "Descartar" + '</a><div style=margin-bottom:6px><b>' + EscapeHtml(j) + '</b></div><div style=margin-bottom:6px>' + EscapeHtml(node.sessions.help[j]) + '</div></div>'; } }
            if (x != '') {
                xxdialogTag = 'HELPREQ-' + node._id;
                setModalContent('xxAddAgent', "Solicitudes de Ayuda" + ' - ' + EscapeHtml(node.name), x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
            return false;
        }

        function closeDeviceHelpRequest(appid, nodeid) {
            setDialogMode(0);
            meshserver.send({ action: 'msg', type: 'localapp', nodeid: decodeURIComponent(nodeid), appid: decodeURIComponent(appid), value: { cmd: 'cancelhelp' } });
        }

        // Show currently active sessions on this device
        function showDeviceSessions(nodeid, force, e) {
            if (e) haltEvent(e);
            if (xxdialogMode && !force) return false;
            var node = null, x = '';
            if (nodeid == null) { node = currentNode; } else { node = getNodeFromId(nodeid); }
            if ((node == null) || (node.sessions == null)) { setDialogMode(0); return false; }
            for (var i in node.sessions) {
                if ((i == 'kvm') && (node.sessions.multidesk == null)) {
                    x += '<u>' + "Escritorio Remoto" + '</u>';
                    for (var j in node.sessions.kvm) {
                        if (j.startsWith('user/')) {
                            var trash = '';
                            if (((j == userinfo._id) || (GetNodeRights(node) == 0xFFFFFFFF))) { trash = ' <i role=button onclick=\'return endDeviceSession("kvm", "' + encodeURIComponentEx(node._id) + '", "' + encodeURIComponentEx(j) + '")\' title="' + "Desconectar esta sesión" + '" class="fa-fw fa-solid fa-trash text-danger" ></i>'; }
                            x += addHtmlValue2(getUserName(j), ((node.sessions.kvm[j] == 1) ? "1 sesión" : nobreak(format("{0} sesiones", node.sessions.kvm[j]))) + trash);
                        } else if (j == 'busy') {
                            x += addHtmlValue2("El dispositivo está ocupado", ((node.sessions.kvm[j] == 1) ? "1 sesión" : nobreak(format("{0} sesiones", node.sessions.kvm[j]))));
                        }
                    }
                } else if (i == 'multidesk') {
                    x += '<u>' + "Escritorio Remoto" + '</u>';
                    for (var j in node.sessions.multidesk) {
                        var trash = '';
                        if ((j == userinfo._id) || (GetNodeRights(node) == 0xFFFFFFFF)) { trash = ' <i role=button onclick=\'return endDeviceSession("multidesk", "' + encodeURIComponentEx(node._id) + '", "' + encodeURIComponentEx(j) + '")\' title="' + "Desconectar esta sesión" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.multidesk[j] == 1) ? "1 sesión" : nobreak(format("{0} sesiones", node.sessions.multidesk[j]))) + trash);
                    }
                } else if (i == 'terminal') {
                    x += '<u>' + "Terminal" + '</u>';
                    for (var j in node.sessions.terminal) {
                        var trash = '';
                        if ((j == userinfo._id) || (GetNodeRights(node) == 0xFFFFFFFF)) { trash = ' <i role=button onclick=\'return endDeviceSession("terminal", "' + encodeURIComponentEx(node._id) + '", "' + encodeURIComponentEx(j) + '")\' title="' + "Desconectar esta sesión" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.terminal[j] == 1) ? "1 sesión" : nobreak(format("{0} sesiones", node.sessions.terminal[j]))) + trash);
                    }
                } else if (i == 'files') {
                    x += '<u>' + "Archivos" + '</u>';
                    for (var j in node.sessions.files) {
                        var trash = '';
                        if ((j == userinfo._id) || (GetNodeRights(node) == 0xFFFFFFFF)) { trash = ' <i role=button onclick=\'return endDeviceSession("files", "' + encodeURIComponentEx(node._id) + '", "' + encodeURIComponentEx(j) + '")\' title="' + "Desconectar esta sesión" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.files[j] == 1) ? "1 sesión" : nobreak(format("{0} sesiones", node.sessions.files[j]))) + trash);
                    }
                } else if (i == 'tcp') {
                    x += '<u>' + "Enrutamiento TCP" + '</u>';
                    for (var j in node.sessions.tcp) {
                        var trash = '';
                        if ((j == userinfo._id) || (GetNodeRights(node) == 0xFFFFFFFF)) { trash = ' <i role=button onclick=\'return endDeviceSession("tcp", "' + encodeURIComponentEx(node._id) + '", "' + encodeURIComponentEx(j) + '")\' title="' + "Desconectar esta sesión" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.tcp[j] == 1) ? "1 sesión" : nobreak(format("{0} sesiones", node.sessions.tcp[j]))) + trash);
                    }
                } else if (i == 'udp') {
                    x += '<u>' + "Enrutamiento UDP" + '</u>';
                    for (var j in node.sessions.udp) {
                        var trash = '';
                        if ((j == userinfo._id) || (GetNodeRights(node) == 0xFFFFFFFF)) { trash = ' <i role=button onclick=\'return endDeviceSession("udp", "' + encodeURIComponentEx(node._id) + '", "' + encodeURIComponentEx(j) + '")\' title="' + "Desconectar esta sesión" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.udp[j] == 1) ? "1 sesión" : nobreak(format("{0} sesiones", node.sessions.udp[j]))) + trash);
                    }
                }
            }
            if (x != '') {
                xxdialogTag = 'SESSIONS-' + node._id;
                setModalContent('xxAddAgent', "Sesiones" + ' - ' + EscapeHtml(node.name), x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            } else {
                setDialogMode(0);
            }
            return false;
        }

        function endDeviceSession(protocol, nodeid, userid) {
            var userIdSplit = decodeURIComponent(userid).split('/'), uid = userIdSplit[0] + '/' + userIdSplit[1] + '/' + userIdSplit[2], guestname = null;
            if ((userIdSplit.length == 4) && (userIdSplit[3].startsWith('guest:'))) { guestname = atob(userIdSplit[3].substring(6)); }
            if (protocol == 'multidesk') {
                meshserver.send({ action: 'endDesktopMultiplex', nodeid: decodeURIComponent(nodeid), xuserid: uid, guestname, guestname });
            } else {
                meshserver.send({ action: 'msg', type: 'endtunnel', nodeid: decodeURIComponent(nodeid), xuserid: uid, guestname, guestname, protocol: protocol });
            }
        }

        // Show currently active sessions on this device
        function showDeviceMessages(nodeid, force, e) {
            if (e) haltEvent(e);
            if (xxdialogMode && !force) return false;
            var node = null, x = '<div style=max-height:200px;overflow-y:auto>', count = 0;
            if (nodeid == null) { node = currentNode; } else { node = getNodeFromId(nodeid); }
            if ((node == null) || (node.sessions == null) || (node.sessions.msg == null)) { setDialogMode(0); return false; }
            for (var i in node.sessions.msg) {
                var msg = i, icon = 5;
                if (typeof node.sessions.msg[i].msg == 'string') { msg = node.sessions.msg[i].msg; }
                if ((typeof node.sessions.msg[i].msgid == 'number') && (eventsMessageId[node.sessions.msg[i].msgid] != null)) { msg = eventsMessageId[node.sessions.msg[i].msgid]; }
                if (typeof node.sessions.msg[i].icon == 'number') { icon = node.sessions.msg[i].icon; }
                if ((icon < 1) || (icon > 9)) { icon = 5; }
                x += '<table style=width:96%><td style=width:24px><div class=NotifyIconSmall' + icon + '></div><td><div style="border-radius:5px;background-color:#BBB;width:100%;padding:8px">' + EscapeHtml(msg) + '</div></table>';
                count++;
            }
            x += '</div>';
            if (count > 0) {
                xxdialogTag = 'MESSAGES-' + node._id;
                setModalContent('xxAddAgent', "Mensajes del Agente" + ' - ' + EscapeHtml(node.name), x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
            return false;
        }

        function toggleCollapseGroup(id, id2, type) {
            var x;
            if (type == 2) {
                // Table rows collapse
                var xrows = document.getElementsByName('xxdevice2');
                if (xrows.length == 0) return;
                var rows = [];
                for (var i = 0; i < xrows.length; i++) { if (xrows[i].attributes.colname.value.substring(7) == id2) { rows.push(xrows[i]); } }
                var x = (rows[0].style['display'] == 'none');
                if (x) { delete CollapsedGroups[id2]; } else { CollapsedGroups[id2] = true; }
                for (var i = 0; i < rows.length; i++) { rows[i].style['display'] = (x ? '' : 'none'); }
            } else {
                // Simple DIV collapse
                x = (QS('DevxCol' + id)['display'] == 'none');
                if (x) { delete CollapsedGroups[id2]; } else { CollapsedGroups[id2] = true; }
                QV('DevxCol' + id, x);
            }
            if (x) {
                QC('DevxColImg' + id).remove('fa-chevron-right');
                QC('DevxColImg' + id).add('fa-chevron-down');
            } else {
                QC('DevxColImg' + id).remove('fa-chevron-down');
                QC('DevxColImg' + id).add('fa-chevron-right');
            }
            putstore('_collapse', JSON.stringify(CollapsedGroups));
            updateCollapseAllButton();
            onDevicesScrollEx();
            onDevicesScrollEx(); // TODO: Not sure why, but second call is needed for proper update
        }

        function toggleKvmDevice(node) {
            if (typeof node == 'string') { node = getNodeFromId(node); } // Convert nodeid to node if needed
            var rights = GetNodeRights(node);
            if ((rights & 8) || (rights & 256)) { // Requires remote control rights or desktop view only rights
                //var conn = 0;
                //if ((node.conn & 1) != 0) { conn = 1; } else if ((node.conn & 6) != 0) { conn = 2; } // Check what type of connect we can do (Agent vs AMT)
                if (node.conn & 1) { connectMultiDesktop(node, 1); }
            }
        }

        function getUserShortStr(node) {
            if (node == null || node.users == null || (!Array.isArray(node.users)) || node.users.length == 0) return '';
            if (node.users.length > 1) { return '<span title="' + EscapeHtml(node.users.join(', ')) + '">' + nobreak(format("{0} usuarios", node.users.length)) + '</span>'; }
            var u = node.users[0], su = u, i = u.indexOf('\\');
            if (i > 0) { su = u.substring(i + 1); }
            var upn = node.upnusers && node.upnusers[0];
			su = EscapeHtml((features3 & 0x00000002) && upn != null ? upn : su);
            if(features3 & 0x00000002){
                if (su.length > 29) { su = su.substring(0, 28) + '&#8230;'; }
            } else {
                if (su.length > 15) { su = su.substring(0, 14) + '&#8230;'; }
            }
            if (node.lusers && node.lusers.length > 0) {
                return addKeyLinkConditional(su, EscapeHtml(u) + ' (' + "Bloqueada" + ')', (node.lusers && node.lusers.indexOf(u) >= 0));
            } else {
                return '<span title="' + EscapeHtml(u) + '">' + su + '</span>';
            }
        }

        function autoConnectDesktops() { if (Q('autoConnectDesktopCheckbox').checked == true) { connectAllKvmFunction(); } }
        function connectAllKvmFunction(force) {
            if (xxdialogMode) return false;
            if (force !== true) { // We need to count how many devices will need to be connected, if it's a lot, prompt first.
                var count = 0;
                for (var i in nodes) {
                    var node = nodes[i], nodeid = nodes[i]._id;
                    if ((multiDesktop[nodeid] == null) && ((Object.keys(checkedNodeids).length == 0) || checkedNodeids[nodeid])) {
                        var rights = GetNodeRights(node);
                        if ((rights & 8) || (rights & 256)) { // Requires remote control rights or desktop view only rights
                            //var conn = 0;
                            //if ((node.conn & 1) != 0) { conn = 1; } else if ((node.conn & 6) != 0) { conn = 2; } // Check what type of connect we can do (Agent vs AMT)
                            if ((node.conn & 1) && (node.v == true)) { count++; }
                        }
                    }
                }
                if (count > 8) {
                    setModalContent('xxAddAgent', "Conectar Todos", format("¿Estás seguro de que desea conectarse a {0} dispositivos?", count));
                    var closeFunc = function(event,a) {
                        document.getElementById('xxAddAgentModal').removeEventListener('hide.bs.modal', closeFunc);
                        Q('autoConnectDesktopCheckbox').checked = false;
                    }
                    document.getElementById('xxAddAgentModal').addEventListener('hide.bs.modal', closeFunc);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => {
                        document.getElementById('xxAddAgentModal').removeEventListener('hide.bs.modal', closeFunc);
                        connectAllKvmFunction(true);
                    });
                    return;
                }
            }

            // Perform connect all
            for (var i in nodes) {
                if ((nodes[i].v == true) && (multiDesktop[nodes[i]._id] == null) && ((Object.keys(checkedNodeids).length == 0) || checkedNodeids[nodes[i]._id])) {
                    toggleKvmDevice(nodes[i]._id);
                }
            }
        }
        function disconnectAllKvmFunction() { if (xxdialogMode) return false; for (var nodeid in multiDesktop) { multiDesktop[nodeid].Stop(); } multiDesktop = {}; }
        function onMultiDesktopStateChange(desk, state) { try { QH('skvmid_' + desk.shortid, ["Desconectado", "Conectando....", "Configurar...", '', ''][state]); } catch (ex) { } }
        function onDeviceSearchChanged(e) {
            var url = new URL(window.location.href);
            if ((e != null) && ((e.target.id == 'SearchInput') || (e.target.id == 'KvmSearchInput'))) { // search box changed or cleared
                if (e.target.id == 'SearchInput') {
                    Q('KvmSearchInput').value = Q('SearchInput').value;
                } else {
                    Q('SearchInput').value = Q('KvmSearchInput').value;
                }
                if (e.target.value != '') {
                    url.searchParams.set('filter', e.target.value);
                    if (urlargs.filter) { urlargs.filter = e.target.value; }
                } else {
                    url.searchParams.delete('filter');
                    if (urlargs.filter) { delete urlargs.filter; }
                }
            } else if ((e != null) && (e.target.id == 'DevFilterSelect')) { // devfilter box changed
                // DO NOTHING
            } else {
                url.searchParams.delete('filter');
                if (urlargs.filter) { delete urlargs.filter; }
            }
            if (((features & 0x10000000) == 0) && (xxcurrentView > 0)) {
                try { window.history.replaceState({}, document.title, decodeURIComponent(url.toString())); } catch (ex) { }
            }
            mainUpdate(5);
        }

        function showMultiDesktopSettings() {
            QV('td7amtkvm', false);
            QV('td7meshkvm', true);
            QV('td7rdpkvm', false);
            QV('d7desktopOtherSettings', false);
            d7bitmapquality.value = multidesktopsettings.quality;
            d7bitmapscaling.value = multidesktopsettings.scaling;
            d7encoding.value = multidesktopsettings.agentencoding;
            if (multidesktopsettings.framerate) { d7framelimiter.value = multidesktopsettings.framerate; } else { d7framelimiter.value = 100; }
            changeDesktopSettingsTab(null, 'd7meshkvm');
            setModalContent('xxAddAgent', "Opciones de Escritorio Remoto", 7);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showMultiDesktopSettingsChanged());
        }

        function showMultiDesktopSettingsChanged() {
            multidesktopsettings.quality = d7bitmapquality.value;
            multidesktopsettings.scaling = d7bitmapscaling.value;
            multidesktopsettings.framerate = d7framelimiter.value;
            multidesktopsettings.agentencoding = d7encoding.value;
            localStorage.setItem('multidesktopsettings', JSON.stringify(multidesktopsettings));
            // Make changes to all current connections
            for (var i in multiDesktop) { multiDesktop[i].m.SendCompressionLevel(multidesktopsettings.agentencoding, multidesktopsettings.quality, multidesktopsettings.scaling, multidesktopsettings.framerate); }
        }

        function connectMultiDesktop(node, contype) {
            var nodeid = node._id, shortid = nodeid.split('/')[2];
            var desk = multiDesktop[nodeid];
            if (desk == null) {
                if (Q('kvmid_' + shortid) == null) return; // Check if this device is being displayed, if not, exit now.
                if (contype == 2) {
                    // Setup the Intel AMT remote desktop
                    if ((node.intelamt.user == null) || (node.intelamt.user == '')) { return; }
                    desk = CreateAmtRedirect(CreateAmtRemoteDesktop('kvmid_' + shortid), authCookie);
                    desk.shortid = shortid;
                    //desk.debugmode = debugmode;
                    desk.onStateChanged = onMultiDesktopStateChange;
                    desk.m.bpp = 1;
                    desk.m.useZRLE = true;
                    desk.m.showmouse = true;
                    desk.m.onKvmData = function (data) { console.log('KVM Data received in multi-desktop mode, this is not supported.'); }; // KVM Data Channel not supported in multi-desktop right now.
                    desk.m.onScreenSizeChange = mdeskAdjust; // Multi-Desktop Adjust
                    desk.Start(nodeid, 16994, '*', '*', 0);
                    desk.contype = 2;
                    multiDesktop[nodeid] = desk;
                } else if (contype == 1) {
                    // Setup the Mesh Agent remote desktop
                    desk = CreateAgentRedirect(meshserver, CreateAgentRemoteDesktop('kvmid_' + shortid), serverPublicNamePort, authCookie, authRelayCookie, domainUrl);
                    desk.m.UseExtendedKeyFlag = (node.agent.id < 5); // Only use extended keys on Windows agents for now
                    desk.m.mouseCursorActive(xxcurrentView == 11);
                    desk.shortid = shortid;
                    desk.attemptWebRTC = attemptWebRTC;
                    desk.webrtcconfig = webrtcconfiguration;
                    desk.onStateChanged = onMultiDesktopStateChange;
                    //desk.onConsoleMessageChange = function () { console.log('CONSOLEMSG:', desk.consoleMessage); }
                    desk.m.ImageType = multidesktopsettings.agentencoding; // Send 4 if WebP is supported, otherwise send 1 for JPEG.
                    desk.m.CompressionLevel = multidesktopsettings.quality;
                    desk.m.ScalingLevel = multidesktopsettings.scaling;
                    if (multidesktopsettings.framerate) { desk.m.FrameRateTimer = multidesktopsettings.framerate; }
                    if (multidesktopsettings.swapmouse) { desk.m.SwapMouse = multidesktopsettings.swapmouse; }
                    if (multidesktopsettings.rmw) { desk.m.ReverseMouseWheel = multidesktopsettings.rmw; }
                    if (multidesktopsettings.remotekeymap == true) { desk.m.remoteKeyMap = multidesktopsettings.remotekeymap; }
                    //desk.m.onDisplayinfo = deskDisplayInfo;
                    desk.m.onScreenSizeChange = mdeskAdjust; // Multi-Desktop Adjust
                    //if (debugmode > 0) { desk.m.onScreenSizeChange = mdeskAdjust; }
                    desk.Start(nodeid);
                    desk.contype = 1;
                    multiDesktop[nodeid] = desk;
                }
            } else {
                // Disconnect and clean up the remote desktop
                desk.Stop();
                delete multiDesktop[nodeid];
            }
        }

        function getMeshActions(mesh, meshrights) {
            if ((meshrights & 4) == 0) return '';
            var r = '';
            if (mesh.mtype == 1) {
                if ((features & 1) == 0) { // If not WAN-Only
                    r += ' <a href=# role="button" title="' + "Agrega una nueva computadora Intel&reg; AMT localizada en la red Local." + '" onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "Agregar Local" + '</a>';
                    r += ' <a href=# role="button" title="' + "Agrega una nueva computadora Intel&reg; AMT escaneando la red Local." + '" onclick=\'return addAmtScanToMesh("' + mesh._id + '")\'>' + "Escanear Red" + '</a>';
                }
                if (mesh.amt && (mesh.amt.type > 0)) { // CCM Deactivate, CCM or ACM activation, Full Automatic
                    r += ' <a href=# role="button" title="' + "Realizar la activación y configuración de Intel&reg; AMT." + '" onclick=\'return showAmtSetup("' + mesh._id + '")\'>' + "Preparar" + '</a>';
                }
            }
            if ((mesh.mtype == 2) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { // Agent device group
                r += ' <a href=# role="button" title="' + "Agrega una nueva computadora a este grupo de dispositivos instalando el Agente Mesh." + '" onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "Agregar Agente" + '</a>';
                if ((features & 2) == 0) { r += ' <a href=# role="button" title="' + "Invita a alguien a instalar el Agente Mesh en este grupo de dispositivos." + '" onclick=\'return inviteAgentToMesh("' + mesh._id + '")\'>' + "Invitar" + '</a>'; }
            }
            if (mesh.mtype == 3) { // Local device group
                r += ' <a href=# role="button" title="' + "Agrega un dispositivo ubicado en la red local." + '" onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "Agregar Dispositivo" + '</a>';
            }
            //if (mesh.amt && (mesh.amt.type > 2)) { // ACM activation or Full Automatic
            //    r += ' <a href=# style=cursor:pointer;font-size:small title="' + "Switch Intel AMT to Admin Control Mode (ACM)." + '" onclick=\'return showAmtAcmSetup()\'>' + "ACM" + '</a>';
            //}
            return r;
        }

        function addLocalDeviceToMesh(meshid) {
            if (xxdialogMode) return false;
            var mesh = meshes[meshid];
            var x = format("Agrega un dispositivo local al grupo de dispositivos \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Nombre del Dispositivo", '<input id=dp1devicename class=form-control maxlength=32 autocomplete=off onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Nombre de Host", '<input id=dp1hostname class=form-control maxlength=32 autocomplete=off placeholder="' + "Igual que el nombre del dispositivo" + '" onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Tipo", '<select id=dp1type class=form-select><option value=4>' + "Windows (RDP)" + '</option><option value=6>' + "Linux (SSH / SCP / VNC)" + '</option><option value=29>' + "macOS (SSH / SCP / VNC)" + '</option></select>');
            setModalContent('xxAddAgent', "Agregar dispositivo local", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => addLocalDeviceToMeshEx(3, meshid));
            validateLocalDeviceToMesh();
            Q('dp1devicename').focus();
        }

        function validateLocalDeviceToMesh() {
            QE('idx_dlgOkButton', Q('dp1devicename').value.length > 0);
        }

        function addLocalDeviceToMeshEx(button, meshid) {
            var host = Q('dp1hostname').value;
            if (host == '') host = Q('dp1devicename').value;
            meshserver.send({ action: 'addlocaldevice', meshid: meshid, devicename: Q('dp1devicename').value, hostname: host, type: parseInt(Q('dp1type').value) });
        }

        function addDeviceToMesh(meshid) {
            if (xxdialogMode) return false;
            var mesh = meshes[meshid];
            var x = format("Agrega un nuevo dispositivo Intel&reg; AMT al grupo de dispositivos \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Nombre del Dispositivo", '<input id=dp1devicename class="form-control" maxlength=32 autocomplete=off onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Nombre de Host", '<input id=dp1hostname class="form-control" maxlength=32 autocomplete=off placeholder="' + "Igual que el nombre del dispositivo" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Nombre de Usuario", '<input id=dp1username class="form-control" maxlength=32 autocomplete=off placeholder="' + "administrador" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Contraseña", '<input id=dp1password type=password class="form-control" autocomplete=off maxlength=32 onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Seguridad", '<select id=dp1tls class="form-select"><option value=0>' + "Sin Seguridad TLS" + '</option><option value=1>' + "Seguridad TLS requerida" + '</option></select>');

            setModalContent('xxAddAgent', "Agregar dispositivo Intel&reg; AMT", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => addDeviceToMeshEx(3, meshid));

            validateDeviceToMesh();
            Q('dp1devicename').focus();
            return false;
        }

        // Intel AMT device import for a device group
        // function showAmtImport(meshid) {
        //     if (xxdialogMode) return false;
        //     var x = '<form method=post enctype=multipart/form-data action=amtimport.ashx target=fileUploadFrame><input type=text name=link style=display:none id=amtImportDevGroup value="' + meshid + '" /><input type=file name=files id=amtImportInput style=width:100% onchange="p5updateUploadDialogOk(\'p5uploadinput\')" /><input type=hidden name=authCookie value=' + authCookie + ' /><input type=submit id=p5loginSubmit style=display:none /></form>';
        //     setDialogMode(2, "Import Intel AMT devices", 3, p5uploadFileEx, x);
        //     p5updateUploadDialogOk('amtImportInput');
        // }

        function showAmtImport(meshid) {
            if (xxdialogMode) return;
            var x = "Crea muchos dispositivos Intel&reg; AMT al mismo tiempo importando un archivo JSON con el siguiente formato:" + '<br /><pre>[\r\n  {\r\n    "fqdn":"mycomputer.com",\r\n    "uuid":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",\r\n    "version":"12.0.1",\r\n    "username":"admin",\r\n    "password":"password",\r\n    "tls":true\r\n  }\r\n]</pre><input type=file id=d4importFile class="form-control" accept=".json" onchange=showAmtImportValidate() />';
            setModalContent('xxAddAgent', "Importacion de dispositivos Intel&reg; AMT.", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showAmtImportEx(3, meshid));
            QE('idx_dlgOkButton', false);
        }

        function showAmtImportValidate() {
            QE('idx_dlgOkButton', Q('d4importFile').value != null);
        }

        function showAmtImportEx(b, meshid) {
            var fr = new FileReader();
            fr.onload = function (r) {
                var j = null;
                try {
                    j = JSON.parse(r.target.result);
                } catch (ex) {
                    setModalContent('xxAddAgent', "Importacion de dispositivos Intel&reg; AMT.", format("Archivo JSON inválido: {0}.", ex));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    return;
                }
                if (j != null) {
                    meshserver.send({ action: 'importamtdevices', meshid: meshid, amtdevices: j });
                } else {
                    setModalContent('xxAddAgent', "Importacion de dispositivos Intel&reg; AMT.", "Formato de Archivo JSON no válido.");
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            };
            fr.readAsText(Q('d4importFile').files[0]);
        }

        // Intel AMT activation and configuration for agentless device groups
        function showAmtSetup(meshid) {
            if (xxdialogMode) return false;
            var servername = serverinfo.name, mesh = meshes[meshid];
            if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
            var url, domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1);
            if (serverinfo.https == true) {
                var portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port);
                url = 'wss://' + servername + portStr + domainUrl;
            } else {
                var portStr = (serverinfo.port == 80) ? '' : (':' + serverinfo.port);
                url = 'ws://' + servername + portStr + domainUrl;
            }
            var x = format("Agrega, activa y configura Intel&reg; AMT en el grupo \"{0}\" ejecutando periódicamente MeshCmd como administrador en el dispositivo remoto.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += '<textarea readonly=readonly style=width:100%;resize:none;height:100px;overflow:auto;font-size:12px readonly>meshcmd amtconfig --url ' + url + 'apf.ashx --id \'' + meshid.split('/')[2] + '\' --serverhttpshash ' + serverinfo.tlshash + '</textarea>';
            if (serverinfo.amtAcmFqdn != null) {
                x += ('<div style=margin-top:8px>' + "Para la activación de ACM, Intel&reg; AMT deberá configurarse con el siguiente FQDN de confianza:" + ' <b>' + serverinfo.amtAcmFqdn.join(', ') + '</b></div>');
            }
            setModalContent('xxAddAgent', "Configuración de Intel&reg; AMT", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            Q('idx_dlgOkButton').focus();
            return false;
        }

        // Intel AMT ACM activation using setup.bin
        function showAmtAcmSetup() {
            if (xxdialogMode) return false;
            var x = '<table><tr><td><img src=images/usbkey70.png height=70 width=31 style=margin-left:4px;margin-right:8px><td><div>' + "Active Intel&reg; AMT en el modo de control de administrador (ACM) mediante una llave USB formateada FAT. Coloque setup.bin en él e inicie una o más computadoras con esta clave." + '</div><div style=margin-top:6px>' + "Comienza ingresando la contraseña MEBx nueva y anterior." + '</div></table>';
            x += addHtmlFormFloating("Contraseña Anterior", '<input id=dp1password0 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Nueva Contraseña*", '<input id=dp1password1 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Nueva Contraseña*", '<input id=dp1password2 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            if ((features2 & 0x00000020) && (currentMesh.mtype == 1) && (serverinfo.amtProvServerMeshId == currentMesh._id)) { x += '<label><input id=dp1lanprov type=checkbox class="form-check-input me-2" /> ' + "Úselo para la activación de LAN completa." + '</label>'; } // Intel AMT LAN provisioning server is active.
            x += '<div><span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 caracteres, 1 superior, 1 inferior, 1 numérico, 1 no alfanumérico." + '</span></div>';
            setModalContent('xxAddAgent', "Intel&reg; AMT ACM", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', showAmtAcmSetupEx);
            Q('dp1password0').focus();
            validateAmtAcmSetupEx();
        }

        function validateAmtAcmSetupEx() {
            var p0 = Q('dp1password0').value, p1 = Q('dp1password1').value, p2 = Q('dp1password2').value, ok = true;
            if ((p0 != 'admin') && (checkPasswordRequirements(p0, { min: 8, max: 16, numeric: 1, lower: 1, upper: 1, nonalpha: 1 }) == false)) { ok = false; }
            if ((p1 != p2) || (checkPasswordRequirements(p1, { min: 8, max: 16, numeric: 1, lower: 1, upper: 1, nonalpha: 1 }) == false)) { ok = false; }
            QE('idx_dlgOkButton', ok);
        }

        function showAmtAcmSetupEx() {
            var baremetal = ((features2 & 0x00000020) && (currentMesh.mtype == 1) && (serverinfo.amtProvServerMeshId == currentMesh._id) && (Q('dp1lanprov').checked));
            meshserver.send({ action: 'amtsetupbin', oldmebxpass: Q('dp1password0').value, newmebxpass: Q('dp1password1').value, baremetal: baremetal });
        }

        // Display the Intel AMT scanning dialog box
        function addAmtScanToMesh(meshid) {
            if (xxdialogMode) return false;
            var x = "Ingresa un rango de direcciones IP para buscar dispositivos Intel&reg; AMT." + '<br /><br />';
            var amtscanoptions = '';//decodeURIComponent('{{{amtscanoptions}}}').split(',');
            if (amtscanoptions != '') {
                x += '<datalist id=iprangelist>';
                for (var i in amtscanoptions) { x += '<option>' + amtscanoptions[i] + '</option>'; }
                x += '</datalist>';
                x += addHtmlValue("Rango de IP", '<table style=width:250px><tr><td style=width:99%><input id=dp1range list=iprangelist style=width:90% placeholder="192.168.1.0/24" onkeyup=addAmtScanToMeshKeyUp(event) onselect=addAmtScanToMeshKeyUp() onclick=addAmtScanToMeshKeyUp() /><td style=width:1%><input id=dp1rangebutton type=button value="' + "Escanear" + '" onclick=addAmtScanToMeshButton()></input></tr></table>');
            } else {
                x += addHtmlValue("Rango de IP", '<table style=width:250px><tr><td style=width:99%><input id=dp1range style=width:100% list=iprangelist value="192.168.1.0/24" onkeyup=addAmtScanToMeshKeyUp(event) /><td style=width:1%><input id=dp1rangebutton type=button value="' + "Escanear" + '" onclick=addAmtScanToMeshButton()></input></tr></table>');
            }
            x += '<div id=dp1results style="width:100%;height:200px;background-color:white;border:1px gray solid;overflow-y:scroll"></div>';
            x += '<input type=hidden id=amtScanMeshId value="' + meshid + '" />';
            x += '<div style="padding:10px;margin-bottom:5px"><input id="idx_dlgCancelButton2" type="button" value="' + "Cancelar" + '" style="" onclick="dialogclose(0)" /><input id="idx_dlgOkButton2" type="button" value="OK" style="" onclick="dialogclose(1)" /><div><input id="d2scanSelectButton" type="button" value="' + "Seleccionar Todo" + '" onclick="scanSelectAll()" /></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Escanear en busca de dispositivos Intel&reg; AMT", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', addAmtScanToMeshEx);
            QE('idx_dlgOkButton2', false);
            QE('d2scanSelectButton', false);
            QH('dp1results', '<div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Valores de rango de IP de muestra" + '<br />192.168.0.100<br />192.168.1.0/24<br />192.167.0.1-192.168.0.100</div>');
            focusTextBox('dp1range');
            addAmtScanToMeshKeyUp();
            return false;
        }

        function scanSelectAll() {
            var elements = document.getElementsByClassName('DevScanCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { elements[i].checked = true; }
            addAmtScanToMeshCheckbox();
        }

        function addAmtScanToMeshKeyUp(e) {
            QE('dp1rangebutton', (Q('dp1range').value.split('.').length > 3));
            if (e && (e.keyCode == 13)) { haltEvent(e); addAmtScanToMeshButton(); }
        }

        // Called when OK is pressed on the Intel AMT scanning box
        function addAmtScanToMeshEx(button) {
            var elements = document.getElementsByClassName('DevScanCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) {
                if (elements[i].checked) {
                    var ipaddr = elements[i].getAttribute('tag');
                    var amtinfo = amtScanResults[ipaddr];
                    var name = amtinfo.hostname;
                    if (amtinfo.hosttype == 'host') { name = capitalizeFirstLetter(name.split('.')[0]); }
                    meshserver.send({ action: 'addamtdevice', meshid: Q('amtScanMeshId').value, devicename: name, hostname: amtinfo.hostname, amtusername: '', amtpassword: '', amttls: amtinfo.tls });
                }
            }
        }

        // If the user presses the "Scan" button on the Intel AMT scanning dialog box, start a scan.
        function addAmtScanToMeshButton() {
            var range = Q('dp1range').value.trim();
            var rangeSplit = range.split(' ');
            if (rangeSplit.length > 1) { range = rangeSplit[rangeSplit.length - 1]; }
            range = range.trim();
            if (range == '') return;
            QE('dp1range', false);
            QE('dp1rangebutton', false);
            QH('dp1results', '<div style=width:100%;text-align:center;margin-top:20px>' + "Escaneando..." + '</div><div style=width:100%;text-align:center;margin-top:6px;color:#666>' + range + '</div>');
            xxdialogTag = 'AMTSCAN:' + range;
            meshserver.send({ action: 'scanamtdevice', range: range });
        }

        // Called when a scanned computer is checked or unchecked.
        function addAmtScanToMeshCheckbox() {
            var elements = document.getElementsByClassName('DevScanCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked) checkcount++; }
            QE('idx_dlgOkButton2', checkcount > 0);
        }

        // Return true is the input string looks like an email address
        function checkEmail(str) {
            var x = str.split('@');
            var ok = ((x.length == 2) && (x[0].length > 0) && (x[1].split('.').length > 1) && (x[1].length > 2));
            if (ok == true) { var y = x[1].split('.'); for (var i in y) { if (y[i].length == 0) { ok = false; } } }
            return ok;
        }

        function inviteAgentToMesh(meshid) {
            if (xxdialogMode) return false;
            var x = '', mesh = meshes[meshid];
            if (features & 64) {
                x += addHtmlFormFloating("Tipo de Invitación", '<select id=d2InviteType onchange=d2ChangedInviteType() class=form-select><option value=0>' + "Enlace de invitación" + '</option><option value=1>' + "Invitación por correo electrónico" + '</option></select>') + '<hr />';
                x += '<div id=emailInviteDiv style=display:none>' + format("Invita a alguien a instalar el Agente Mesh. Se enviará un correo electrónico con el enlace a la instalación del agente de mesh para el grupo de dispositivos \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
                x += addHtmlFormFloating("Nombre (opcional)", '<input id=agentInviteName value="" placeholder="Name (optional)" class="form-control" maxlength=64 />');
                x += addHtmlFormFloating("Correo electrónico", '<input id=agentInviteEmail placeholder="' + "ejemplo@email.com" + '" class="form-control" onkeyup=validateAgentInvite() />');
                x += addHtmlFormFloating("Sistema Operativo", '<select id=agentInviteNameOs onchange=d2ChangedInviteType() class=form-select><option value=4>' + "Enviar enlace de instalación" + '</option><option value=0 selected>' + "Cualquiera soportado" + '</option><option value=1>' + "Sólo Windows" + '</option><option value=3>' + "Sólo Apple macOS" + '</option><option value=2>' + "Solo Linux" + '</option><option value=5>' + "Asistente MeshCentral" + '</option></select>');
                x += '<div id=d2agentexpirediv>';
                x += addHtmlFormFloating("Expiración del Enlace", '<select id=agentInviteExpire class=form-select><option value=1>' + "1 hora" + '</option><option value=8>' + "8 horas" + '</option><option value=24>' + "1 dia" + '</option><option value=168>' + "1 semana" + '</option><option value=5040>' + "1 mes" + '</option><option value=0>' + "Ilimitado" + '</option></select>');
                x += '</div>';
                x += '<div id=d2agentInstallTypeDiv2>';
                x += addHtmlFormFloating("Tipo de Instalación", '<select id=agentInviteType class=form-select><option value=0>' + "Segundo plano e interactivo" + '</option><option value=2>' + "Segundo plano solamente" + '</option><option value=1>' + "Sólo Interactivo" + '</option></select>');
                x += '</div>';
                x += addHtmlFormFloating("Mensaje" + ' ' + "(opcional)", '<textarea id="agentInviteMessage" class="form-control" style="height:100px;resize:none;" maxlength="1024"></textarea>');
                x += '</div>';
            }
            x += '<div id=urlInviteDiv>' + format("Invita a alguien a instalar el Agente Mesh compartiendo un enlace de invitación. Este enlace le indica al usuario las instrucciones de instalación para el grupo de dispositivos \"{0}\". El enlace es público y no se necesita una cuenta para este servidor.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Expiración del Enlace", '<select id=d2inviteExpire class=form-select onchange=d2RequestInvitationLink()><option value=1>' + "1 hora" + '</option><option value=8>' + "8 horas" + '</option><option value=24>' + "1 dia" + '</option><option value=168>' + "1 semana" + '</option><option value=5040>' + "1 mes" + '</option><option value=0>' + "Ilimitado" + '</option></select>');
            x += addHtmlFormFloating("Agentes", '<select id=d2agentType class=form-select onchange=d2ChangedInviteType()><option value=0>' + "Todos los agentes disponibles" + '</option><option value=1>' + "Agente de malla de Windows" + '</option><option value=2>' + "Agente de malla de Linux" + '</option><option value=4>' + "Agente de malla de macOS" + '</option><option value=8>' + "Asistente MeshCentral" + '</option><option value=16>' + "Android MeshAgent" + '</option></select>');
            x += '<div id=d2agentInstallTypeDiv>';
            x += addHtmlFormFloating("Tipo de Instalación", '<select id=d2agentInviteType class=form-select onchange=d2RequestInvitationLink()><option value=0>' + "Segundo plano e interactivo" + '</option><option value=2>' + "Segundo plano solamente" + '</option><option value=1>' + "Sólo Interactivo" + '</option></select>');
            x += '</div>';
            x += '<div id=agentInvitationLinkDiv style="text-align:center;font-size:large;margin:16px;display:none"><a href=# id=agentInvitationLink rel="noreferrer noopener" target="_blank" style=cursor:pointer></a> <img src=images/link4.png height=10 width=10 title="' + "Copiar enlance al portapapeles" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Invitar", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                performAgentInvite(3, meshid);
            });
            if (features & 64) { Q('d2InviteType').focus(); d2ChangedInviteType(); } else { Q('d2inviteExpire').focus(); validateAgentInvite(); }
            d2RequestInvitationLink();
            return false;
        }

        function d2RequestInvitationLink() {
            meshserver.send({ action: 'createInviteLink', meshid: xxdialogTag, expire: parseInt(Q('d2inviteExpire').value), flags: parseInt(Q('d2agentInviteType').value), agents: parseInt(Q('d2agentType').value) });
        }

        function d2ChangedInviteType() {
            if (features & 64) {
                QV('urlInviteDiv', Q('d2InviteType').value == 0);
                QV('d2agentexpirediv', Q('agentInviteNameOs').value == 4);
                QV('d2agentInstallTypeDiv2', Q('agentInviteNameOs').value < 2);
                QV('emailInviteDiv', Q('d2InviteType').value == 1);
            }
            QV('d2agentInstallTypeDiv', parseInt(Q('d2agentType').value) < 2);
            validateAgentInvite();
            d2RequestInvitationLink();
        }

        function d2CopyInviteToClip() { copyTextToClip(Q('agentInvitationLink').href); }

        function d2CopySecretToClip() { copyTextToClip(Q('d2optsecret').getAttribute('secret')); }

        function validateAgentInvite() {
            if ((features & 64) && (Q('d2InviteType').value == 1)) {
                QE('idx_dlgOkButton', checkEmail(Q('agentInviteEmail').value));
                QV('idx_dlgCancelButton', true);
            } else {
                QE('idx_dlgOkButton', true);
                QV('idx_dlgCancelButton', false);
            }
        }

        function performAgentInvite(button, meshid) {
            if ((features & 64) && (Q('d2InviteType').value == 1)) {
                meshserver.send({ action: 'inviteAgent', meshid: meshid, email: Q('agentInviteEmail').value, name: Q('agentInviteName').value, os: Q('agentInviteNameOs').value, flags: Q('agentInviteType').value, msg: Q('agentInviteMessage').value, expire: parseInt(Q('agentInviteExpire').value) });
            }
        }

        function addAgentToMesh(meshid) {
            if (xxdialogMode) return false;
            var mesh = meshes[meshid], x = '', installType = 0, moreoptions = '';

            var opts = '<select id=aginsSelect onchange=addAgentToMeshClick() class=form-select><option value=0>' + "Windows" + '</option><option value=1>' + "Linux / BSD" + '</option><option value=5>' + "Instalador Binario para Linux / BSD / macOS" + '</option><option value=2>' + "Apple macOS" + '</option>';
            if ((features & 2) == 0) { opts += '<option value=6>' + "Dispositivo móvil" + '</option>'; } // Don't display mobile setup in LAN mode.
            opts += '<option value=7>' + "Asistente MeshCentral" + '</option>';
            opts += '<option value=3>' + "Windows (DesInstalar)" + '</option><option value=4>' + "Linux / BSD (DesInstalar)" + '</option><option value=8>' + "Apple macOS (Uninstall)" + '</option></select>';
            x += addHtmlFormFloating("Sistema Operativo", opts);

            var servername = serverinfo.name;
            if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
            var domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1), portStr = '';
            if (serverinfo.https == true) { portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port); } else { portStr = (serverinfo.port == 80) ? '' : (':' + serverinfo.port); }

            // Add Linux/macOS binary installer option
            var binaryInstallAgentsOrder = [6, 5, 10005, 25, 26, 28, 30, 32, 36, 37, 40, 41, 45, 16, 29];
            var binaryInstallAgents = { 6 : 'Linux x86-64', 5 : 'Linux x86-32', 10005 : 'Apple OSX Universal', 25 : 'Linux ARM-HF, Rasberry Pi', 26 : 'Linux ARM64-HF', 28: 'Linux MIPS24KC (OpenWRT)', 30 : 'FreeBSD x86-64', 32: 'Linux ARM 64 bit', 36: 'OpenWRT x86-64', 37: 'OpenBSD x86-64', 40: 'Linux MIPSEL24KC (OpenWRT)', 41: 'ARMADA/CORTEX-A53/MUSL (OpenWRT)', 45: 'RISC-V 64bit', 16: 'Apple macOS x86-64', 29: 'Apple macOS ARM-64' };
            for (var i in binaryInstallAgentsOrder) { moreoptions += '<option value="' + binaryInstallAgentsOrder[i] + '">' + binaryInstallAgents[binaryInstallAgentsOrder[i]] + '</option>' }
            x += '<div id="aginsSysTypeDiv">';
            x += addHtmlFormFloating("Tipo de Sistema", '<select id="aginsSysType" onchange="addAgentToMeshClick()"  class="form-select">' + moreoptions + '</select>');
            x += '</div>';

            // Add agent and assistant installation type option
            x += '<div id="aginsTypeDiv">';
            x += addHtmlFormFloating("Tipo de Instalación", '<select id="aginsType" onchange="addAgentToMeshClick()" class="form-select"><option value="0">' + "Segundo plano & interactivo" + '</option><option value="2">' + "Segundo plano solamente" + '</option><option value="1">' + "Sólo Interactivo" + '</option></select>');
            x += '</div><div id="asinsTypeDiv">';
            x += addHtmlFormFloating("Tipo de Instalación", '<select id="asinsType" onchange="addAgentToMeshClick()" class="form-select"><option value="2">' + "Aplicación, Conectar a petición del usuario" + '</option><option value="3">' + "Aplicación, Siempre Conectada" + '</option><option value="0">' + "Bandeja del Sistema, Conectar con el consentimiento del usuario" + '</option><option value="1">' + "Bandeja del Sistema, Siempre conectada" + '</option><option value="4">' + "Bandeja del Sistema, Solo Monitoriear" + '</option></select>');
            x += '</div><hr>';

            // \/:*?"<>|
            var meshfilename = mesh.name
            meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');

            // Windows agent install
            x += '<div id=agins_windows>' + format("Para agregar una nueva computadora al grupo de dispositivos \"{0}\", descarga el agente MeshCentral e instálalo en la computadora a administrar. Este agente tiene la información del servidor y grupo de dispositivos incrustada dentro de él.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline id=aginsw32lnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "version x86 32bit del MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar el URL del agente x86 32bit a la papelera" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline id=aginsw64lnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "version x86 64bit del MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar el URL del agente Windows x86 64bit a la papelera" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline id=aginswa64lnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Version ARM 64bit de el MeshAgent" + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar el URL del agente Windows ARM 64bit a la papelera" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            if (debugmode > 0) { x += addHtmlValue("Archivo de Opciones", '<a id=aginswmshlnk name="meshsettings?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + format("{0} opciones (.msh)", EscapeHtml(mesh.name)) + '</a>'); }
            x += '</div>';

            // Linux agent install
            x += '<div id=agins_linux style=display:none>' + format("Para agregar una computadora a \"{0}\" ejecuta el siguiente comando. Se necesitarán credenciales de root.", EscapeHtml(mesh.name)) + '<br />';
            x += '<textarea id=agins_linux_area rows=2 cols=20 readonly=readonly style=border-radius:4px;padding:6px;margin-top:4px;margin-bottom:4px;border:0;width:100%;resize:none;height:160px;overflow:auto;font-size:12px class=form-control></textarea>';
            x += '<div style=font-size:x-small;float:right>' + "* Para BSD, ejecute primero \"pkg install wget sudo bash\"." + '</div><a style=text-decoration:none title="' + "Copiar al portapapeles" + '" onclick=copyAgentIdValue("agins_linux_area")>' + "Copiar" + ' <i class="fa-regular fa-clipboard" style=cursor:pointer></i></a></div>';

            // macOS agent install
            x += '<div id=agins_osx style=display:none>' + format("Para agregar una nueva computadora al grupo de dispositivos \"{0}\", descarga el agente de malla e instálalo en la computadora a administrar. Este instalador del agente tiene la información del servidor y grupo de dispositivos incrustada dentro de él.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=16&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "x86-64bit version of macOS Mesh Agent" + '">' + "macOS x86-64bit" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar URL del Agente para macOS al portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=16&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=29&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "ARM 64bit version of macOS Mesh Agent" + '">' + "macOS ARM (64bit)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar URL del Agente para macOS al portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=29&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Universal version of macOS Mesh Agent" + '">' + "macOS (Universal)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar URL del Agente para macOS al portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += '</div>';

            // QR code agent install
            x += '<div id=agins_qrcode style=display:none;min-height:180px><a id=agins_qrimage_a rel="noreferrer noopener" target=_blank><div id=agins_qrimage style=float:right;margin-left:10px;width:180px;height:180px;cursor:pointer></div></a><div>' + format("Para agregar un dispositivo móvil al grupo \"{0}\", descarga la aplicación MeshAgent y escana éste código QR.", EscapeHtml(mesh.name)) + '</div>';
            x += '<table style=width:180px>';
            x += '<tr><td style=text-align:center><a title="' + "Google Play Store" + '"rel="noreferrer noopener" target=_blank href="https://play.google.com/store/apps/details?id=com.meshcentral.agent2"><img style=cursor:pointer src="images/google-play-140.png" width=140 srcset="images/google-play-280.png 2x" /></a></td></tr>';
            x += '<tr><td style=text-align:center><a title="' + "Amazon App Store" + '" rel="noreferrer noopener" target=_blank href="https://www.amazon.co.uk/gp/product/B097Z4Q7SK/"><img style=cursor:pointer src="images/amazon-appstore-140.png"  width=140 srcset="images/amazon-appstore-280.png 2x" /></a></td></tr>';
            x += '</table>';
            x += addHtmlValue("Android APK", '<a class=text-decoration-underline onclick=downloadFile("meshagents?id=14' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '",null,true) title="' + "APK version of the MeshAgent" + '">' + "APK" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar URL al portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=14&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '")></i>');
            x += '</div>'

            // MeshCentral Assistant
            x += '<div id=agins_assistant style=display:none><table><tr><td style=vertical-align:top><div>' + format("El Asistente de MeshCentral es una herramienta de Windows que los usuarios pueden utilizar para solicitar ayuda. Usa el enlace de abajo para descargar la versión que se conectará con el Grupo de Dispositivos \"{0}\".", EscapeHtml(mesh.name)) + '</div><div></div><br />';
            x += '<a class=text-decoration-underline id=asinslnk name="meshagents?id=10006&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Asistente MeshCentral para Windows" + '">' + "Asistente para Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar URL al portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=10006&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '")></i>';
            x += '</td><td><img id=agass_img1 src=images/assistant-100.png width=74 height=100 srcset="images/assistant-200.png 2x"><img id=agass_img2 src=images/assistant2-100.png width=74 height=100 srcset="images/assistant2-200.png 2x"></td></tr></table></div>';

            // MeshCentral Assistant2
            x += '<div id=agins_assistant2 style=display:none><table><tr><td style=vertical-align:top><div>' + "El Asistente de MeshCentral es una herramienta de Windows que los usuarios pueden utilizar para solicitar ayuda. Usa el enlace de abajo para descargar la versión que monitoreará el agente en modo background." + '</div><div></div><br />';
            x += '<a class=text-decoration-underline id=asinslnk2 name="meshagents?id=10006&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Asistente MeshCentral para Windows" + '">' + "Asistente para Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar URL al portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=10006&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '")></i>';
            x += '</td><td><img src=images/assistant-100.png width=74 height=100 srcset="images/assistant-200.png 2x"></td></tr></table></div>';

            // Windows agent uninstall
            x += '<div id=agins_windows_un style=display:none>' + "Para eliminar un Agente Mesh, descarga el archivo a continuación, ejecútalo y haz clic en \"desinstalar\"." + '<br /><br />';
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline id=aginsw32unlnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "version x86 32bit del MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar el URL del agente x86 32bit a la papelera" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline id=aginsw64unlnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "version x86 64bit del MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar el URL del agente Windows x86 64bit a la papelera" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline id=aginswa64unlnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Version ARM 64bit de el MeshAgent" + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar el URL del agente Windows ARM 64bit a la papelera" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += '</div>';

            // Linux agent uninstall
            x += '<div id=agins_linux_un style=display:none>' + "Para eliminar un Agente Mesh, ejecuta el siguiente comando. Se necesitarán credenciales de root." + '<br />';
            x += '<textarea id=agins_linux_area_un rows=2 cols=20 readonly=readonly style=border-radius:4px;padding:6px;margin-top:4px;margin-bottom:4px;border:0;width:100%;resize:none;height:160px;overflow:auto;font-size:12px  class=form-control></textarea>';
            x += '<a style=text-decoration:none title="' + "Copiar al portapapeles" + '" onclick=copyAgentIdValue("agins_linux_area_un")>Copy <i class="fa-regular fa-clipboard" style=cursor:pointer></i></a></div>';

            // macOS agent uninstall
            x += '<div id=agins_osx_un style=display:none>' + "Para eliminar un Agente Mesh, descarga el archivo a continuación, extrae el archivo ZIP, luego haz clic derecho en \"Uninstall.command\" y selecciona \"Abrir\"." + '<br /><br />';
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Universal version of macOS Mesh Agent" + '">macOS Agent (Universal)</a> <i class="fa-regular fa-clipboard" title="' + "Copiar URL del Agente para macOS al portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += '</div>';

            // Linux binary installer
            x += '<div id=agins_linux_inst style=display:none>' + "This is an executable on OSs with graphical user interfaces." + '<br /><br />' + "Apple macOS executables will need to be removed from quarantine by running 'xattr -r -d com.apple.quarantine meshagent'." + '<br /><br />' + "You need to run 'chmod +x meshagent', and then run this file." + '<br /><br />';
            x += addHtmlValue("Agente Mesh", '<a class=text-decoration-underline id=aginsbinlnk name="meshagents?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + "meshagent" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copiar la URL del agente en el portapapeles" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Comando" + '&nbsp;<i class="fa-regular fa-clipboard" title="' + "Copiar la URL del agente en el portapapeles" + '" style=cursor:pointer onclick=copyAgentIdValue("aginsbincmd")></i>', '<input id=aginsbincmd type=text class="form-control" readonly value=\'wget -O meshagent' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' \"https://' + servername + portStr + domainUrl + 'meshagents?id=' + meshid.split('/')[2].split('$').join('%24').split('@').join('%40') + '\' />', ['col-md-4 d-flex align-items-center', 'col-md-8']);
            x += '</div>';

            setModalContent('xxAddAgent', "Agregar Mesh Agent", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');

            // Create the QR code
            new QRCode(Q('agins_qrimage'), { text: serverinfo.magenturl + ',' + serverinfo.agentCertHash + ',' + meshid.split('/')[2], width: 180, height: 180, colorDark: '#000000', colorLight: '#EEE', correctLevel: QRCode.CorrectLevel.M });
            Q('agins_qrimage_a').setAttribute('href', serverinfo.magenturl + ',' + serverinfo.agentCertHash + ',' + meshid.split('/')[2])

            if ((features & 0x2000) == 0) {
                Q('agins_linux_area').value = '(wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '"' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' -O ./meshinstall.sh || wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1" --no-proxy' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' -O ./meshinstall.sh) && chmod 755 ./meshinstall.sh && sudo -E ./meshinstall.sh https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\' || ./meshinstall.sh https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\'\r\n';
                Q('agins_linux_area_un').value = '(wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '"' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' -O ./meshinstall.sh || wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1" --no-proxy' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' -O ./meshinstall.sh) && chmod 755 ./meshinstall.sh && sudo -E ./meshinstall.sh uninstall https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\' || ./meshinstall.sh uninstall https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\'\r\n';
                //Q('agins_linux_area_un').value = '(wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1' + (urlargs.key?('&key=' + urlargs.key):'') + '"' + (((features & 0x80000000) != 0)?' --no-check-certificate':'') + ' -O ./meshinstall.sh || wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1" --no-proxy' + (((features & 0x80000000) != 0)?' --no-check-certificate':'') + ' -O ./meshinstall.sh) && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh uninstall\r\n';
            }
            else {
                // Server asked that agent be installed to preferably not use a HTTP proxy.
                Q('agins_linux_area').value = 'wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" --no-proxy' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo -E ./meshinstall.sh https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\' || ./meshinstall.sh https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\'\r\n';
                Q('agins_linux_area_un').value = 'wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" --no-proxy' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo -E ./meshinstall.sh uninstall https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\' || ./meshinstall.sh uninstall https://' + servername + portStr + domainUrlNoSlash + ' \'' + meshid.split('/')[2] + '\'\r\n';
                //Q('agins_linux_area_un').value = 'wget "https://' + servername + portStr + domainUrl + 'meshagents?script=1' + (urlargs.key?('&key=' + urlargs.key):'') + '" --no-proxy' + (((features & 0x80000000) != 0)?' --no-check-certificate':'') + ' -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh uninstall\r\n';
            }
            Q('aginsSelect').focus();
            addAgentToMeshClick();
            return false;
        }

        var xxModal;

        function setModalContent(modalId, title, bodyContent, size = null) {
            // Check if the modal elements exist
            var modalConfigurableElement = document.getElementById('xxAddAgentModalConf');
            var modalTitleElement = document.getElementById(modalId + 'Title');

            if (modalConfigurableElement && modalTitleElement) {
                // Set the modal title
                modalTitleElement.innerHTML = title;

                // Set the modal body content based on the bodyContent Type
                if (typeof bodyContent == 'number') {
                    for (var i = 1; i < 24; i++) { QV('dialog' + i, i == bodyContent); } // Edit this line when more dialogs are added
                    xxdialogMode = i;
                } else if (typeof bodyContent == 'string'){
                    Q('dialog2').innerHTML = bodyContent;
                    for (var i = 1; i < 24; i++) { QV('dialog' + i, i == 2); } // Edit this line when more dialogs are added
                    xxdialogMode = 2;
                }
                // If size is provided, set the modal size class
                if (size) {
                    modalConfigurableElement.classList.remove('modal-sm', 'modal-lg', 'modal-xl'); // Remove any existing size classes
                    switch (size) {
                        case 'small':
                            modalConfigurableElement.classList.add('modal-sm');
                            break;
                        case 'large':
                            modalConfigurableElement.classList.add('modal-lg');
                            break;
                        case 'extra-large':
                            modalConfigurableElement.classList.add('modal-xl');
                            break;
                        // Add any additional size options here
                        default:
                            break;
                    }
                } else {
                    modalConfigurableElement.classList.remove('modal-sm', 'modal-lg', 'modal-xl'); // Remove any existing size classes
                }
            } else {
                console.error('Modal elements not found:', modalId + 'Title', modalId + 'Body');
            }
        }

        /**
        * Function to show and hide a modal.
        *
        * @param {string} modalId - The ID of the modal to display.
        * @param {string} okButtonId - The ID of the button that triggers the OK action.
        * @param {function} okCallback - A callback function that runs when the OK button is clicked.
        *        If the callback returns `false`, the modal will remain open.
        *        Any other return value (or no return) will close the modal by default.
        * @param {*} [b=null] - An optional parameter passed to the callback.
        * @param {*} [tag=null] - Another optional parameter passed to the callback.
        */
        function showModal(modalId, okButtonId, okCallback, b = null, tag = null) {
            if (xxModal == null) {
                ['idx_dlgOkButton', 'idx_dlgCancelButton', 'idx_dlgDeleteButton'].forEach(function (id) { // clear existing onclick, enable buttons and show them
                    Q(id).onclick = null;
                    QE(id, (id != 'idx_dlgDeleteButton' ? true : false));
                    QV(id, (id != 'idx_dlgDeleteButton' ? true : false));
                });
                xxModal = new bootstrap.Modal(document.getElementById(modalId));
                var hiddenModal = function (event) {
                    if (xxModal) { xxModal.dispose(); xxModal = null; }
                    if (xxdialogMode != 0) { xxdialogMode = 0; }
                    document.getElementById(modalId).removeEventListener('hidden.bs.modal', hiddenModal);
                }
                document.getElementById(modalId).addEventListener('hidden.bs.modal', hiddenModal);
            }
            xxModal.show();
            document.getElementById(okButtonId).onclick = function () {
                if (typeof okCallback === 'function' && okCallback) {
                    xxdialogMode = 0; // reset xxdialogMode back to 0 because we might be opening up another dialog and it checks if one is already open
                    var callbackResult = okCallback(b, tag);
                    // Close the modal by default unless `false` is explicitly returned from the callback function
                    if (callbackResult !== false) { xxModal.hide(); }
                } else {
                    if (xxModal) { xxModal.hide(); }
                }
            };
        }

        function copyAgentIdValue(id) {
            copyTextToClip(Q(id).value);
            // Show notification
            //showNotification('center', null, 'Copied to clipboard', 'success', null, 3000, '300px', '30px');
        }

        function showNotification(position, title, text, icon, confirmButtonText, timer, width, height) {
            Swal.fire({
                position: position,
                title: title,
                text: text,
                icon: icon,
                confirmButtonText: confirmButtonText,
                timer: timer,
                width: width,
                showConfirmButton: confirmButtonText !== null
            });
        }

        function copyAgentUrl(url, addflag) {
            var servername = serverinfo.name;
            if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
            var domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1);
            var portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port);
            var c = 'https://' + servername + portStr + domainUrl + url;
            if (addflag == 1) c += Q('aginsType').value;
            c += (urlargs.key ? ('&key=' + urlargs.key) : '');
            if (Q('aginsSelect').value == 5) { c += '&meshinstall=' + Q('aginsSysType').value; }
            if (Q('aginsSelect').value == 7) { c += '&ac=' + Q('asinsType').value; }
            copyTextToClip(c);
        }

        function addAgentToMeshClick() {
            var v = Q('aginsSelect').value;
            QV('agins_windows', v == 0);
            QV('agins_linux', v == 1);
            QV('agins_osx', v == 2);
            QV('agins_windows_un', v == 3);
            QV('agins_linux_un', v == 4);
            QV('agins_linux_inst', v == 5);
            QV('aginsSysTypeDiv', v == 5);
            QV('agins_qrcode', v == 6);
            QV('agins_assistant', (v == 7) && (Q('asinsType').value != 4));
            QV('agins_assistant2', (v == 7) && (Q('asinsType').value == 4));
            QV('agins_osx_un', v == 8);
            Q('aginsbinlnk').onclick = function () { downloadFile((Q('aginsbinlnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : '') + '&meshinstall=' + Q('aginsSysType').value); };
            Q('aginsbincmd').value = (Q('aginsbincmd').value.split('&installflags=')[0]) + '&installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : '') + '&meshinstall=' + Q('aginsSysType').value + '\"';
            QV('aginsTypeDiv', (v == 0) || (v == 5));
            QV('asinsTypeDiv', (v == 7));

            // Fix the links if needed
            Q('aginsw32lnk').onclick = function () { downloadFile((Q('aginsw32lnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : ''), null, true); }
            Q('aginsw64lnk').onclick = function () { downloadFile((Q('aginsw64lnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : ''), null, true); }
            Q('aginswa64lnk').onclick = function () { downloadFile((Q('aginswa64lnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : ''), null, true); }
            Q('aginsw32unlnk').onclick = function () { downloadFile((Q('aginsw32lnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : ''), null, true); }
            Q('aginsw64unlnk').onclick = function () { downloadFile((Q('aginsw64lnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : ''), null, true); }
            Q('aginswa64unlnk').onclick = function () { downloadFile((Q('aginswa64lnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : ''), null, true); }
            if (debugmode > 0) { Q('aginswmshlnk').onclick = function () { downloadFile((Q('aginswmshlnk').name.split('installflags=')[0]) + 'installflags=' + Q('aginsType').value + (urlargs.key ? ('&key=' + urlargs.key) : ''), null, true); } }
            Q('asinslnk').onclick = function () { downloadFile(Q('asinslnk').name + '&ac=' + Q('asinsType').value, null, true); }
            Q('asinslnk2').onclick = function () { downloadFile(Q('asinslnk').name + '&ac=' + Q('asinsType').value, null, true); }
            if (v == 7) {
                QV('agass_img1', Q('asinsType').value < 2);
                QV('agass_img2', Q('asinsType').value >= 2);
            }
        }

        function validateDeviceToMesh() {
            QE('idx_dlgOkButton', (Q('dp1devicename').value.length > 0) && (passwordcheck(Q('dp1password').value)));
        }

        function addDeviceToMeshEx(button, meshid) {
            var amtuser = Q('dp1username').value;
            if (amtuser == '') amtuser = 'admin';
            var host = Q('dp1hostname').value;
            if (host == '') host = Q('dp1devicename').value;
            meshserver.send({ action: 'addamtdevice', meshid: meshid, devicename: Q('dp1devicename').value, hostname: host, amtusername: amtuser, amtpassword: Q('dp1password').value, amttls: Q('dp1tls').value });
        }

        function deviceHeaderSet() {
            if (deviceHeaderId == 0) { deviceHeaderId = 1; return; }
            deviceHeaders['DevxHeader' + deviceHeaderId] = ((deviceHeaderTotal == 1) ? "1 nodo" : format("{0} nodos", deviceHeaderTotal));
            //var title = '';
            //for (x in deviceHeaderCount) { if (title.length > 0) title += ', '; title += deviceHeaderCount[x] + ' ' + PowerStateStr2(x); }
            //deviceHeadersTitles["DevxHeader" + deviceHeaderId] = title;
            deviceHeaderId++;
            deviceHeaderCount = {};
            deviceHeaderTotal = 0;
        }

        var powerStateStrings = ['', '<span title="' + "El dispositivo está encendido." + '">' + "Encendido" + '</span>', '<span title="' + "El dispositivo está en estado de suspensión (S1)." + '">' + "Suspendiendo" + '</span>', '<span title="' + "El dispositivo está en estado de suspensión (S2)." + '">' + "Suspendiendo" + '</span>', '<span title="' + "El dispositivo está en estado de reposo (S3)." + '">' + "Estado de Reposo" + '</span>', '<span title="' + "El dispositivo está en estado de hibernacion (S4)." + '">' + "Hibernando" + '</span>', '<span title="' + "El dispositivo está apagado (S5)." + '">' + "Soft-Off" + '</span>', '<span title="' + "El dispositivo se detectó pero no se pudo obtener el estado de alimentación." + '">' + "Presente" + '</span>', '<span title="' + "El dispositivo está apagado." + '">' + "Apagado" + '</span>'];
        var powerStateStrings2 = ['', "El dispositivo está encendido", "El dispositivo está en estado de suspensión (S1)", "El dispositivo está en estado de suspensión (S2)", "El dispositivo está en estado de reposo (S3)", "El dispositivo está hibernando (S4)", "El dispositivo está en estado de apagado (S5)", "El dispositivo está presente, pero no se puede determinar el estado de energía", "El dispositivo está apagado"];
        var powerColorTable = ['pwsTransparent', 'pwsBlack', 'pwsBlue', 'pwsBlue2', 'pwsLightblue', 'pwsBlueviolet', 'pwsDarkgreen', 'pwsLightseagreen', 'pwsLightseagreen2'];
        function NodeStateStr(node) {
            var states = [];
            if (node.state > 0 && node.state < powerStatetable.length) state.push(powerStatetable[node.state]);
            if (node.conn) {
                if ((node.conn & 1) != 0) {
                    if (node.mtype == 4) {
                        if (node.porttype == 'PDU') {
                            states.push('<span title="' + "El interruptor de encendido está listo para usar." + '">' + "Cambiar" + '</span>');
                        } else {
                            states.push('<span title="' + "El puerto IP-KVM está activo y listo para usar." + '">' + "IP-KVM" + '</span>');
                        }
                    } else {
                        states.push('<span title="' + "El Agente Mesh está conectado y listo para su uso." + '">' + "Agente" + '</span>');
                    }
                }
                if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA está conectado y listo para su uso." + '">' + "CIRA" + '</span>'); }
                else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT es enrutable." + '">' + "AMT" + '</span>'); }
                if ((node.conn & 8) != 0) { states.push('<span title="' + "Se puede alcanzar al Agente Mesh utilizando otro agente en modo Relay." + '">' + "Relé" + '</span>'); }
                if ((node.conn & 16) != 0) { states.push('<span title="' + "La conexión MQTT al dispositivo está activa." + '">' + "MQTT" + '</span>'); }
            }
            if ((node.pwr != null) && (node.pwr != 0)) { states.push(powerStateStrings[node.pwr]); }
            if (states.length == 0) return '&nbsp;';
            return states.join(', ');
        }

        function PowerStateStr(x) {
            if (x < powerStatetable.length) return powerStatetable[x];
            return '';
        }

        function PowerStateStr2(x) {
            if ((x != 0) && (x < powerStatetable.length)) return powerStatetable[x];
            return "Desconocido";
        }

        function updateCollapseAllButton() {
            var x = (Object.keys(CollapsedGroups).length > 0)
            QV('CollapseAllButton', !x);
            QV('ExpandAllButton', x);
            QV('CollapseAllButton2', !x);
            QV('ExpandAllButton2', x);
        }

        function selectallButtonFunction() {
            var elements = document.getElementsByClassName('DeviceCheckbox'), checkcount = Object.keys(checkedNodeids).length;
            for (var i = 0; i < elements.length; i++) { elements[i].checked = (checkcount == 0); }
            checkedNodeids = {};
            if (checkcount == 0) {
                checkedNodeids = {};
                var devdivs = document.getElementsByName('xxdevice' + Q('viewselect').value);
                // Checking that the parent style is null will insure that "select all" does not select any devices in collapsed groups
                for (var i = 0; i < devdivs.length; i++) { if ((devdivs[i].parentElement.attributes.style == null) || (devdivs[i].parentElement.attributes.style.value.indexOf('display') < 0)) { checkedNodeids[devdivs[i].id.substring(3)] = 1; } }
            }
            p1updateInfo();
        }

        function p1devcheck(e) {
            if (e.srcElement.checked) { checkedNodeids[e.srcElement.value.substring(6)] = 1; } else { delete checkedNodeids[e.srcElement.value.substring(6)]; }
            p1updateInfo();
        }

        function p1updateInfo() {
            var checkcount = Object.keys(checkedNodeids).length;
            if (checkcount > 0) {
                QE('GroupActionButton', true);
                Q('SelectAllButton').value = "No seleccionar nada";
                QV('cxmdesktop', true);
            } else {
                QE('GroupActionButton', false);
                Q('SelectAllButton').value = "Seleccionar Todo";
                QV('cxmdesktop', false);
            }

            // Custom UI
            if (customui != null) {
                if (customui.devicesbarbuttons) {
                    for (var i in customui.devicesbarbuttons) {
                        if (customui.devicesbarbuttons[i].selection == 'one') { QE('cui:' + i, checkcount == 1); }
                        if (customui.devicesbarbuttons[i].selection == 'many') { QE('cui:' + i, checkcount >= 1); }
                    }
                }
            }
        }

        function groupActionFunction() {
            var addedOptions = [], nodeids = getCheckedDevices(), added = 0;

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0)) {
                setModalContent('xxAddAgent', "Seguridad de la Cuenta", "No se puede acceder a esta función hasta que se habilite la autenticación de dos factores. Esto es necesario para mayor seguridad. Ve a la pestaña \"Mi cuenta\" y revisa la sección \"Seguridad de la cuenta\".");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return;
            }

            // Check if any of the selected devices have a MQTT connection active
            if (features & 0x00400000) {
                for (var i in nodeids) { if ((getNodeFromId(nodeids[i]).conn & 16) != 0) { addedOptions.push({ v: 103, name: "Enviar Mensaje MQTT" });; break; } }
            }

            // Display the options that are allowed depending on what devices are selected.
            addedOptions.push({ v: 105, name: "Exportar información de dispositivos", s: true });
            for (var i in nodeids) {
                var node = getNodeFromId(nodeids[i]), rights = GetNodeRights(node);
                if ((rights & 1) && ((added & 2) == 0)) { added |= 2; addedOptions.push({ v: 102, name: "Mover al grupo de dispositivos" }); }
                if (((node.conn & 1) != 0) && ((rights & 0x8000) != 0) && ((added & 1) == 0)) { added |= 1; addedOptions.push({ v: 104, name: "Desinstalar Agente" }); }
                if ((rights & 64) && ((added & 8) == 0)) { added |= 8; addedOptions.push({ v: 100, name: "Despertar dispositivos" }); }
                if ((rights & 262144) && ((added & 4) == 0)) { added |= 4; addedOptions.push({ v: 4, name: "Suspender dispositivos" }); addedOptions.push({ v: 3, name: "Reiniciar dispositivos" }); addedOptions.push({ v: 2, name: "Apagar dispositivos" }); }
                if ((rights & 131072) && ((added & 16) == 0)) { added |= 16; addedOptions.push({ v: 106, name: "Ejecutar comandos" }); }
                if ((rights & 16384) && ((added & 32) == 0)) { added |= 32; addedOptions.push({ v: 108, name: "Notificar dispositivo" }); }
                if ((rights & 4) && ((added & 64) == 0)) { added |= 64; addedOptions.push({ v: 107, name: "Editar etiquetas" }); }
                if ((rights & 8) && ((added & 256) == 0)) { added |= 256; addedOptions.push({ v: 109, name: "Subir archivos" }); }
                if ((rights & 32768) && ((added & 128) == 0)) { added |= 128; addedOptions.push({ v: 101, name: "Eliminar dispositivos" }); }
                if ((node.agent != null) && (features2 & 0x00000010) && (rights == 0xFFFFFFFF) && ((added & 512) == 0)) {
                    added |= 512;
                    addedOptions.push({ v: 110, name: "Forzar actualización del agente" });
                    addedOptions.push({ v: 111, name: "Borrar núcleo del agente" });
                    addedOptions.push({ v: 112, name: "Cargar el archivo de núcleo predeterminado" });
                }
            }

            var addedOptionsStr = '';
            addedOptions.sort(nameSort);
            for (var i in addedOptions) { addedOptionsStr += '<option value=' + addedOptions[i].v + (addedOptions[i].s ? ' selected' : '') + '>' + addedOptions[i].name + '</option>'; }
            var x = "Selecciona una operación para realizar en todos los dispositivos seleccionados. Las acciones se realizarán solo con los derechos correspondientes." + '<br /><br />';
            x += addHtmlFormFloating("Operación", '<select id=d2groupop class="form-select">' + addedOptionsStr + '</select>');

            setModalContent('xxAddAgent', "Acción de Grupo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', groupActionFunctionEx);
        }

        // Get the list of checked devices
        function getCheckedDevices() { return Object.keys(checkedNodeids); }

        function uncheckAllDevices() {
            var elements = document.getElementsByClassName('DeviceCheckbox');
            for (var i = 0; i < elements.length; i++) { elements[i].checked = false; }
            checkedNodeids = {};
        }

        function groupActionFunctionEx() {
            var op = Q('d2groupop').value;
            if (op == 100) {
                // Group wake
                meshserver.send({ action: 'wakedevices', nodeids: getCheckedDevices() });
                uncheckAllDevices();
                return true;
            } else if (op == 101) {
                // Group delete, ask for confirmation
                var chkNodeIds = getCheckedDevices(), x = '';
                if (chkNodeIds.length == 0) return;
                else if (chkNodeIds.length == 1) { x += "¿Confirmar la eliminación del dispositivo seleccionado?" + '<br /><br />'; }
                else {
                    x += format("¿Confirmar la eliminación de los {0} dispositivos seleccionados?" + '<br />', chkNodeIds.length);
                    var nconn = 0, ndisc = 0;
                    for (var i in chkNodeIds) { var n = getNodeFromId(chkNodeIds[i]); if ((n.conn != null) && (n.conn > 0)) { nconn++; } else { ndisc++; } }
                    var y = '';
                    if (nconn == 1) { y += format("1 dispositivo seleccionado está en línea." + '<br />', nconn); }
                    else if (nconn > 1) { y += format("{0} dispositivos seleccionados están en línea." + '<br />', nconn); }
                    if (ndisc == 1) { y += format("1 dispositivo seleccionado está fuera de línea." + '<br />', ndisc); }
                    else if (ndisc > 1) { y += format("{0} dispositivos seleccionados están sin conexión." + '<br />', ndisc); }
                    if (y != '') { x += '<div style=padding:8px>' + y + '</div>'; } else { x += '<br />'; }
                }
                x += '<label><input id=d2check type=checkbox class="form-check-input me-2" onchange=d2groupActionFunctionDelCheck() />' + "Confirmar" + '</label>';
                setModalContent('xxAddAgent', "Eliminar dispositivos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionDelExec());
                QE('idx_dlgOkButton', false);
            } else if (op == 102) {
                // Move computers to a different group
                p10showChangeGroupDialog(getCheckedDevices());
            } else if (op == 103) {
                // Send MQTT Message
                p10showSendMqttMsgDialog(getCheckedDevices());
            } else if (op == 104) {
                // Uninstall agent
                p10showSendUninstallAgentDialog(getCheckedDevices());
            } else if (op == 105) {
                // Export device information
                p2downloadDeviceInfo();
            } else if (op == 106) {
                // Run commands
                d2runCommandDialog({ nodeids: getCheckedDevices(), title: "Ejecutar comandos en los dispositivos seleccionados.", func: uncheckAllDevices });
            } else if (op == 107) {
                // Edit tags
                var x = "Realizar operación de etiquetado de dispositivos por lotes" + '<br /><br />';
                x += addHtmlFormFloating("Operación", '<select id=d2deviceop class="form-select"><option value=1>' + "Agregar etiquetas" + '</option><option value=2>' + "Establecer etiquetas" + '</option><option value=3>' + "Remover etiquetas" + '</option></select>');
                x += addHtmlFormFloating("Etiquetas", '<select id=dp10devicevalue multiple class="form-control" maxlength=4096>');
                // Get a list of all possible device tags
                var allTags = [], y = '';
                for (var i in nodes) { if (nodes[i].tags) { for (var j in nodes[i].tags) { if (allTags.indexOf(nodes[i].tags[j]) == -1) { allTags.push(nodes[i].tags[j]); } } } }
                if (allTags.length > 0) {
                    allTags.sort();
                    for (var i in allTags) {
                        var tag = EscapeHtml(allTags[i]);
                        y += '<option>' + EscapeHtml(allTags[i]) + '</option>';
                    }
                }
                x += y + '</select>'
                setModalContent('xxAddAgent', "Editar Etiquetas del Dispositivo", x);
                $('#dp10devicevalue').select2({
                    theme: 'bootstrap-5',
                    width: $( this ).data( 'width' ) ? $( this ).data( 'width' ) : $( this ).hasClass( 'w-100' ) ? '100%' : 'style',
                    placeholder: "Etiqueta1, Etiqueta2, Etiqueta3",
                    closeOnSelect: false,
                    allowClear: true,
                    tokenSeparators: [','],
                    tags: true
                });
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionTagsExec());
            } else if (op == 108) {
                // Device notification
                var x = '<div style=margin-bottom:4px>'+ "Realizar notificaciones de dispositivos por lotes" + '</div>';
                x += '<select id=d2deviceop class="form-select-sm me-2" style=width:100%;margin-bottom:4px><option value=2>' + "Notificación Emergente Temporal" + '</option><option value=1>' + "Cuadro de Mensaje" + '</option><option value=3>' + "Alert Box" + '</option></select>';
                x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Título" + '" style=width:100%;box-sizing:border-box;margin-bottom:4px />';
                x += '<textarea id=d2notifyMsg style=width:100%;height:140px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px></textarea>';
                x += '<select style=width:100% id=d2notifyTimeout>';
                x += '<option disabled value="">Only Applicable to Message Box Notifications</option>';
                x += '<option value=2 selected>' + "Show for 2 Minutes (Default)" + '</option>';
                x += '<option value=10>' + "Show for 10 minutes" + '</option>';
                x += '<option value=30>' + "Show for 30 minutes" + '</option>';
                x += '<option value=60>' + "Show for 60 minutes" + '</option>';
                x += '<option value=0>' + "Mostrar mensaje hasta que el usuario lo descarte" + '</option>';
                x += '</select>';
                setModalContent('xxAddAgent', "Notificar Dispositivo", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionNotifyExec());
                Q('d2notifyMsg').focus();
            } else if (op == 109) {
                // Upload files
                var wintype = false, linuxtype = false, chkNodeIds = getCheckedDevices();
                for (var i in chkNodeIds) { var n = getNodeFromId(chkNodeIds[i]); if (n.agent) { if (isWindowsNode(n)) { wintype = true; } else { linuxtype = true; } } }
                var x = "Cargar archivos seleccionados a todos los dispositivos seleccionados" + '<br /><br />';
                x += '<form method=post enctype=multipart/form-data action=uploadfilebatch.ashx target=fileUploadFrame>';
                x += '<input type=hidden name=authCookie value=' + authCookie + ' /><input type=hidden name=nodeIds value=' + chkNodeIds.join(',') + ' /><input type=submit id=d2batchUploadSubmit style=display:none />';
                x += '<input type=file name=files id=d2uploadinput class="form-control" multiple=multiple onchange="d2batchUploadValidate()" /><br /><br />';
                if (wintype) { x += addHtmlFormFloating("Ruta de Windows", '<input type=text class="form-control" onchange="d2batchUploadValidate()" onkeyup="d2batchUploadValidate()" name=winpath id=d2winuploadpath placeholder="C:\\temp" value="" />'); }
                if (linuxtype) { x += addHtmlFormFloating("Ruta de Linux", '<input type=text class="form-control" onchange="d2batchUploadValidate()" onkeyup="d2batchUploadValidate()" name=linuxpath id=d2linuxuploadpath placeholder="/tmp" value="" />'); }
                x += '<br /><label><input type=checkbox class="form-check-input me-2" name=createFolder />' + "¿Crear carpeta si no existe?" + '</label><br />';
                x += '<label><input type=checkbox class="form-check-input me-2" name=overwriteFiles />' + "¿Sobrescribir si el archivo existe?" + '</label></form>';
                setModalContent('xxAddAgent', "Carga de archivos por lotes", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2batchUploadValidateOk());
                d2batchUploadValidate();
            } else if (op == 110) {
                // Force agent update
                var x = "¿Forzar la actualización del agente en los dispositivos seleccionados?" + '<br /><br />';
                setModalContent('xxAddAgent', "Forzar actualización del agente", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentUpdateExec());
            } else if (op == 111) {
                // Clear agent core
                var x = "¿Borrar el núcleo del agente en los dispositivos seleccionados?" + '<br /><br />';
                setModalContent('xxAddAgent', "Borrar núcleo del agente", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentClearCoreExec());
            } else if (op == 112) {
                // Upload default server core
                var x = "¿Cargar el núcleo del servidor predeterminado en los dispositivos seleccionados?" + '<br /><br />';
                setModalContent('xxAddAgent', "Cargar el archivo de núcleo predeterminado", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentDefaultCodeExec());
            } else {
                // Power operation
                meshserver.send({ action: 'poweraction', nodeids: getCheckedDevices(), actiontype: parseInt(op) });
                uncheckAllDevices();
                return true;
            }
            return false;
        }

        function d2batchUploadValidate() { QE('idx_dlgOkButton', (Q('d2uploadinput').files.length != 0) && ((Q('d2winuploadpath') == null) || (Q('d2winuploadpath').value != '')) && ((Q('d2linuxuploadpath') == null) || (Q('d2linuxuploadpath').value != ''))); }
        function d2batchUploadValidateOk() { Q('d2batchUploadSubmit').click(); }
        function d2groupActionFunctionAgentUpdateExec() { meshserver.send({ action: 'updateAgents', nodeids: getCheckedDevices() }); }
        function d2groupActionFunctionAgentClearCoreExec() { meshserver.send({ action: 'uploadagentcore', nodeids: getCheckedDevices(), type: 'clear' }); }
        function d2groupActionFunctionAgentDefaultCodeExec() { meshserver.send({ action: 'uploadagentcore', nodeids: getCheckedDevices(), type: 'default' }); }

        function d2groupActionFunctionNotifyExec() {
            var op = Q('d2deviceop').value, title = Q('dp2notifyTitle').value, msg = Q('d2notifyMsg').value, chkNodeIds = getCheckedDevices(), timeout = parseFloat(Q('d2notifyTimeout').value);
            if (msg.length == 0) return;
            if (title == '') { title = decodeURIComponent('{{{extitle}}}'); }
            if (title == '') { title = "MeshCentral"; }
            if (isNaN(timeout)) { timeout = 2; }
            if (op == 1) { // MessageBox
                for (var i = 0; i < chkNodeIds.length; i++) { meshserver.send({ action: 'msg', type: 'messagebox', nodeid: chkNodeIds[i], title: title, msg: msg, timeout: (timeout * 60000) }); }
            } else if (op == 2) { // Toast
                meshserver.send({ action: 'toast', nodeids: chkNodeIds, title: title, msg: msg });
            } else if (op == 3) { // Old Style MessageBox
                for (var i = 0; i < chkNodeIds.length; i++) { meshserver.send({ action: 'msg', type: 'alertbox', nodeid: chkNodeIds[i], title: title, msg: msg }); }
            }
        }

        function d2groupActionFunctionTagsExec() {
            var chkNodeIds = getCheckedDevices(), op = Q('d2deviceop').value, optags = [];
            var tt = $('#dp10devicevalue').select2('data');
            for (var i in tt) { optags.push(tt[i]['text'].trim()); }
            if (op == 2) { // Set tags
                for (var i in chkNodeIds) { meshserver.send({ action: 'changedevice', nodeid: chkNodeIds[i], tags: optags.join(',') }); }
            } else {
                var taggroup = [];
                for (var i in optags) { var tname = optags[i].trim(); if ((tname.length > 0) && (tname.length < 64) && (taggroup.indexOf(tname) == -1)) { taggroup.push(tname); } }
                for (var i in chkNodeIds) {
                    var nodeTags = null, tagChanges = false, n = getNodeFromId(chkNodeIds[i]);
                    if (n.tags != null) { nodeTags = Clone(n.tags); }
                    if (nodeTags == null) { nodeTags = []; }
                    for (var j = 0; j < optags.length; j++) {
                        if ((op == 1) && (nodeTags.indexOf(optags[j]) == -1)) { nodeTags.push(optags[j]); tagChanges = true; } // Add new tags
                        if (op == 3) { var k = nodeTags.indexOf(optags[j]); if (k >= 0) { nodeTags.splice(k, 1); tagChanges = true; } } // Remove tags
                    }
                    if (tagChanges) { meshserver.send({ action: 'changedevice', nodeid: chkNodeIds[i], tags: nodeTags.join(',') }); }
                }
            }
        }

        function p2downloadDeviceInfo() {
            if (xxdialogMode) return;
            var chkNodeIds = getCheckedDevices(), intelamtpresent = false;
            for (var i in chkNodeIds) { if (getNodeFromId(chkNodeIds[i]).intelamt != null) { intelamtpresent = true; } }
            var x = "Descargar la lista de dispositivos con uno de los formatos de archivo a continuación." + '<br /><br />';
            x += addHtmlValue("Formato CSV", '<a href=# style=cursor:pointer onclick=\'return p2downloadDeviceInfoCSV()\'>' + 'devicelist.csv' + '</a>');
            x += addHtmlValue("Formato JSON", '<a href=# style=cursor:pointer onclick=\'return p2downloadDeviceInfoJSON()\'>' + 'devicelist.json' + '</a>');
            if (intelamtpresent) { x += addHtmlValue("Intel&reg; AMT JSON", '<a href=# style=cursor:pointer onclick=\'return p2downloadAmtInfoJSON()\'>' + 'computerlist.json' + '</a>'); }
            x += '<div style=margin-top:8px><label><input type=checkbox class="form-check-input me-2" id=d2DevInfoDetailsCheck />' + "Incluir detalles del dispositivo" + '</label></div>';
            setModalContent('xxAddAgent', "Exportar Información de Dispositivos", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

        function csvClean(v) {
            if (v == null) return '';
            return v.split(',').join('');
        }

        function p2downloadDeviceInfoCSV() {
            var chkNodeIds = getCheckedDevices();
            if (Q('d2DevInfoDetailsCheck').checked) {
                var tz = null;
                try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (ex) { }
                meshserver.send({ action: 'getDeviceDetails', nodeids: chkNodeIds, tz: tz, tf: new Date().getTimezoneOffset(), l: getLang(), type: 'csv' }); // With details
            } else {
                // Without details
                var csv = 'id,name,rname,host,icon,ip,osdesc,state,groupname,conn,pwr,av,update,firewall,avdetails,tags,lastbootuptime' + '\r\n', r = [];
                for (var i in chkNodeIds) {
                    var n = getNodeFromId(chkNodeIds[i]);
                    csv += '"' + n._id.split(',').join('') + '","' + n.name.split(',').join('') + '","' + (n.rname ? (n.rname.split(',').join('')) : '') + '","' + (n.host ? (n.host.split(',').join('')) : '') + '","' + n.icon + '","' + (n.ip ? n.ip : '') + '","' + (n.osdesc ? (n.osdesc.split(',').join('')) : '') + '","' + n.state + '","' + meshes[n.meshid].name.split(',').join('') + '","' + (n.conn ? n.conn : '') + '","' + (n.pwr ? n.pwr : '') + '"';
                    if (typeof n.wsc == 'object') { csv += ',"' + csvClean(n.wsc.antiVirus) + '","' + csvClean(n.wsc.autoUpdate) + '","' + csvClean(n.wsc.firewall) + '"'; }
                    else if (typeof n.lsc == 'object') { csv += ',"' + csvClean(n.lsc.antiVirus) + '",,"' + csvClean(n.lsc.firewall) + '"'; } else { csv += ',,,'; }
                    if (typeof n.av == 'object') {
                        var avdetails = '', firstav = true;
                        for (var a in n.av) { if (typeof n.av[a].product == 'string') { if (firstav) { firstav = false; } else { avdetails += '|'; } avdetails += (n.av[a].product + '/' + ((n.av[a].enabled) ? 'enabled' : 'disabled') + '/' + ((n.av[a].updated) ? 'updated' : 'notupdated')); } }
                        csv += ',"' + csvClean(avdetails) + '"';
                    }
                    else {
                        csv += ',';
                    }
                    if (typeof n.tags == 'object') {
                        var tagsdetails = '', firsttags = true;
                        for (var a in n.tags) { if (firsttags) { firsttags = false; } else { tagsdetails += '|'; } tagsdetails += n.tags[a]; }
                        csv += ',"' + csvClean(tagsdetails) + '"';
                    }
                    else {
                        csv += ',';
                    }
                    if (typeof n.lastbootuptime == 'number') { csv += ',"' + n.lastbootuptime + '"'; }
                    csv += '\r\n';
                }
                saveAs(stringToUtf8Blob(csv), 'devicelist.csv');
            }
            return false;
        }

        // Download information about selected Intel AMT devices.
        function p2downloadAmtInfoJSON() {
            var chkNodeIds = getCheckedDevices();
            var r = { webappversion: '0.9.0', computers: [] };
            for (var i in chkNodeIds) {
                var node = getNodeFromId(chkNodeIds[i]);
                if (node.intelamt == null) continue;
                var c = { name: node.name, host: node.host, user: node.intelamt.user, pass: '', tls: (node.intelamt.tls == 1) ? 1 : 0, ver: node.intelamt.ver, pstate: node.intelamt.state }
                if (node.intelamt.state == 2) { c.pmode = 1; } // TODO
                if (node.intelamt.realm) { c.digestrealm = node.intelamt.realm; }
                if (node.intelamt.hash) { c.tlscerthash = node.intelamt.hash; }
                r.computers.push(c);
            }
            saveAs(new Blob([JSON.stringify(r, null, 2)], { type: 'application/octet-stream' }), 'computerlist.json');
        }

        function p2downloadDeviceInfoJSON() {
            var chkNodeIds = getCheckedDevices();
            if (Q('d2DevInfoDetailsCheck').checked) {
                var tz = null;
                try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (ex) { }
                meshserver.send({ action: 'getDeviceDetails', nodeids: chkNodeIds, tz: tz, tf: new Date().getTimezoneOffset(), l: getLang(), type: 'json' }); // With details
            } else {
                // Without details
                var r = [];
                for (var i in chkNodeIds) { r.push(getNodeFromId(chkNodeIds[i])); }
                saveAs(new Blob([JSON.stringify(r, null, 2)], { type: 'application/octet-stream' }), 'devicelist.json');
            }
            return false;
        }

        function d2groupActionFunctionDelCheck() { QE('idx_dlgOkButton', Q('d2check').checked); }
        function d2groupActionFunctionDelExec() { meshserver.send({ action: 'removedevices', nodeids: getCheckedDevices() }); uncheckAllDevices(); }

        function d2runCommandDialog(options) {
            var wintype = false, linuxtype = false, agenttype = false;
            for (var i in options.nodeids) {
                var n = getNodeFromId(options.nodeids[i]);
                if (n.agent) {
                    if ((GetNodeRights(n) & 24) == 24) { agenttype = true; }
                    if (isWindowsNode(n)) { wintype = true; } else { linuxtype = true; }
                }
            }
            if ((wintype == true) || (linuxtype == true) || (agenttype == true)) {
                // Fetch run options
                var runopt = { type: 1, runAs: 0, source: 1, cmd: '' };
                try { runopt = JSON.parse(getstore('runopt', runopt)); } catch (ex) { }

                if (options.selectedFile) {
                    var filename = options.selectedFile.name.toLowerCase();
                    console.log('filename', filename);
                    if (filename.endsWith('.bat')) { runopt.type = 1; }
                    if (filename.endsWith('.ps1')) { runopt.type = 2; }
                    if (filename.endsWith('.sh')) { runopt.type = 3; }
                    if (filename.endsWith('.agentconsole')) { runopt.type = 4; }
                }

                var x = '';
                if (options.title) { x += options.title + '<br />'; }
                x += '<select id=d2cmdtype onclick=d2runCommandValidate() style=width:100%;margin-bottom:4px;margin-top:4px class=form-select>';
                if (wintype == true) { x += '<option value=1' + ((runopt.type == 1) ? ' selected' : '') + '>' + "Línea de Comandos de Windows" + '</option><option value=2' + ((runopt.type == 2) ? ' selected' : '') + '>' + "Windows PowerShell" + '</option>'; }
                if (linuxtype == true) { x += '<option value=3' + ((runopt.type == 3) ? ' selected' : '') + '>' + "Shell de comandos Linux/BSD/macOS" + '</option>'; }
                if (agenttype == true) { x += '<option value=4' + ((runopt.type == 4) ? ' selected' : '') + '>' + "Consola de Agente" + '</option>'; } // MESHRIGHT_REMOTECONTROL & MESHRIGHT_AGENTCONSOLE are needed
                x += '</select>';
                x += '<select id=d2cmduser style=width:100%;margin-bottom:4px class=form-select><option value=0' + ((runopt.runAs == 0) ? ' selected' : '') + '>' + "Ejecutar como agente" + '</option><option value=1' + ((runopt.runAs == 1) ? ' selected' : '') + '>' + "Ejecutar como usuario, agente si no hay usuario" + '</option><option value=2' + ((runopt.runAs == 2) ? ' selected' : '') + '>' + "Debe ejecutarse como usuario" + '</option></select>';
                if (options.selectedFile == null) {
                    x += '<select id=d2cmdsource onclick=d2runCommandValidate() style=width:100%;margin-bottom:4px class=form-select><option value=0' + ((runopt.source == 0) ? ' selected' : '') + '>' + "Comandos desde caja de texto" + '</option><option value=1' + ((runopt.source == 1) ? ' selected' : '') + '>' + "Comandos desde un archivo" + '</option>';
                    if (userinfo.siteadmin & 8) { x += '<option value=2' + ((runopt.source == 2) ? ' selected' : '') + '>' + "Comandos desde un archivo en el servidor" + '</option>'; }
                    x += '</select><textarea id=d2runcmd onkeyup=d2runCommandValidate() style=width:100%;height:200px;resize:none;overflow-y:scroll class=form-control>' + (runopt.cmd ? EscapeHtml(decodeURIComponent(runopt.cmd)) : '') + '</textarea>';
                    x += '<div id=d2runfile style=display:none><input id=d2runfileex class="form-control" type=file onchange=d2runCommandValidate() id=d2localFile name=files onchange=d2runCommandValidate() /></div>';
                    if (userinfo.siteadmin & 8) { x += '<div id=d2runsfile style=display:none><div id=d2serveraction valign=bottom><input type=button id=p2FolderUp disabled="disabled" onclick=d3folderup() value="Up" />&nbsp;<span id=p2CurrentFolder></span></div><div id=d2serverfiles></div></div>'; }
                }
                xxdialogTag = options;
                setModalContent('xxAddAgent', "Ejecutar Comandos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    d2groupActionFunctionRunCommands(3, options);
                });
                if (options.selectedFile == null) {
                    Q('d2runcmd').focus();
                    if (userinfo.siteadmin & 8) { d3fileoptions = { dialog: 2, files: 'd2serverfiles', folderup: 'p2FolderUp', currentFolder: 'p2CurrentFolder', func: null }; d3updatefiles(); } // Update the server files
                }
                d2runCommandValidate();
            }
        }

        function d2runCommandValidate() {
            QV('d2cmduser', Q('d2cmdtype').value < 4);
            if (xxdialogTag.selectedFile == null) {
                QV('d2runcmd', Q('d2cmdsource').value == 0);
                QV('d2runfile', Q('d2cmdsource').value == 1);
                QV('d2runsfile', Q('d2cmdsource').value == 2);
                var ok = false;
                if (Q('d2cmdsource').value == 0) { if (Q('d2runcmd').value.length > 0) { ok = true; } } // From text box
                if (Q('d2cmdsource').value == 1) { if (Q('d2runfileex').files.length == 1) { ok = true; } } // From file
                if (Q('d2cmdsource').value == 2) { ok = false; } // From server file
                QE('idx_dlgOkButton', ok);
            } else {
                QE('idx_dlgOkButton', true);
            }
        }

        function d2groupActionFunctionRunCommands(b, options) {
            var type = 3;
            try { type = parseInt(Q('d2cmdtype').value); } catch (ex) { }
            if (options.selectedFile == null) { putstore('runopt', JSON.stringify({ type: type, runAs: parseInt(Q('d2cmduser').value), source: parseInt(Q('d2cmdsource').value), cmd: encodeURIComponent(Q('d2runcmd').value) })); } // Save run options
            var cmd = { action: 'runcommands', nodeids: options.nodeids, type: type, runAsUser: parseInt(Q('d2cmduser').value) };
            if (options.selectedFile) {
                // Drag & drop file
                var reader = new FileReader();
                reader.onload = function (e) { cmd.cmds = e.target.result; meshserver.send(cmd); if (options.func) { options.func(); } }
                reader.readAsText(options.selectedFile);
            } else if (Q('d2cmdsource').value == 0) {
                // From text box
                cmd.cmds = Q('d2runcmd').value;
                meshserver.send(cmd);
                if (options.func) { options.func(); }
            } else if (Q('d2cmdsource').value == 1) {
                // From file
                var reader = new FileReader();
                reader.onload = function (e) { cmd.cmds = e.target.result; meshserver.send(cmd); if (options.func) { options.func(); } }
                reader.readAsText(Q('d2runfileex').files[0]);
            } else if (Q('d2cmdsource').value == 2) {
                // From server file
                var files = d3getFileSel();
                if (files.length != 1) return;
                cmd.cmdpath = d3filetreelocation.join('/') + '/' + files[0];
                meshserver.send(cmd);
                if (options.func) { options.func(); }
            }
        }

        function onSortSelectChange(skipsave) {
            sort = document.getElementById('sortselect').selectedIndex;
            if (!skipsave) { putstore('sort', sort); }
        }

        /*
        function nameSort(a, b) { var aa = a.name.toLowerCase(), bb = b.name.toLowerCase(); return sortCollator.compare(a.namel, b.namel); }
        function meshSort(a, b) {
            if (a.meshnamel > b.meshnamel) return 1;
            if (a.meshnamel < b.meshnamel) return -1;
            if (a.meshid > b.meshid) return 1;
            if (a.meshid < b.meshid) return -1;
            if (showRealNames == true) { if (a.rnamel > b.rnamel) return 1; if (a.rnamel < b.rnamel) return -1; return 0; }
            else { if (a.namel > b.namel) return 1; if (a.namel < b.namel) return -1; }
            return 0;
        }
        function powerSort(a, b) { var ap = a.pwr?a.pwr:0; var bp = b.pwr?b.pwr:0; if (ap > bp) return -1; if (ap < bp) return 1; if (ap == bp) { if (showRealNames == true) { if (a.rnamel > b.rnamel) return 1; if (a.rnamel < b.rnamel) return -1; return 0; } else { if (a.namel > b.namel) return 1; if (a.namel < b.namel) return -1; return 0; } } return 0; }
        function deviceSort(a, b) { if (a.namel > b.namel) return 1; if (a.namel < b.namel) return -1; return 0; }
        function deviceHostSort(a, b) { if (a.rnamel > b.rnamel) return 1; if (a.rnamel < b.rnamel) return -1; return 0; }
        function lastConnectSort(a, b) { var aa = a.lastconnect, bb = b.lastconnect; if (aa == null) { aa = 99999999999999; } if (bb == null) { bb = 99999999999999; } if (a.conn > 0) { aa = 99999999999998; } if (b.conn > 0) { bb = 99999999999998; } if (aa == bb) { return nameSort(a, b); } return (aa - bb); }
        */

        var sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
        function nameSort(a, b) { var aa = a.name.toLowerCase(), bb = b.name.toLowerCase(); return sortCollator.compare(aa, bb); }
        function meshSort(a, b) {
            var x = sortCollator.compare(a.meshnamel, b.meshnamel);
            if (x != 0) return x;
            x = sortCollator.compare(a.meshid, b.meshid);
            if (x != 0) return x;
            if (showRealNames == true) { return sortCollator.compare(a.rnamel, b.rnamel); }
            return sortCollator.compare(a.namel, b.namel);
        }
        function powerSort(a, b) { var ap = a.pwr ? a.pwr : 0; var bp = b.pwr ? b.pwr : 0; if (ap > bp) return -1; if (ap < bp) return 1; if (ap == bp) { if (showRealNames == true) { return sortCollator.compare(a.rnamel, b.rnamel); } else { return sortCollator.compare(a.namel, b.namel); } } }
        function deviceSort(a, b) { return sortCollator.compare(a.namel, b.namel); }
        function deviceHostSort(a, b) { return sortCollator.compare(a.rnamel, b.rnamel); }
        function lastConnectSort(a, b) { var aa = a.lastconnect, bb = b.lastconnect; if (aa == null) { aa = 99999999999999; } if (bb == null) { bb = 99999999999999; } if (a.conn > 0) { aa = 99999999999998; } if (b.conn > 0) { bb = 99999999999998; } if (aa == bb) { return nameSort(a, b); } return (aa - bb); }
        function lastBootUpTimeSort(a, b) { var aa = a.lastbootuptime, bb = b.lastbootuptime; if (aa == null) { aa = 99999999999999; } if (bb == null) { bb = 99999999999999; } if (aa == bb) { return nameSort(a, b); } return (aa - bb); }

        function onSearchFocus(x) { searchFocus = x; }
        function clearDeviceSearch() { Q('KvmSearchInput').value = Q('SearchInput').value = ''; onOnlineCheckBox(); mainUpdate(1); }
        function onMapSearchFocus(x) { mapSearchFocus = x; }
        function onUserSearchFocus(x) { userSearchFocus = x; }
        function onConsoleFocus(x) { consoleFocus = x; }

        function parseSearchAndInput(x) {
            var s = x.split(' ' + "y" + ' '), r = null;
            for (var i in s) {
                var r2 = getDevicesThatMatchFilter(s[i]);
                if (r == null) { r = r2; } else { var r3 = []; for (var j in r2) { if (r.indexOf(r2[j]) >= 0) { r3.push(r2[j]); } } r = r3; }
            }
            return r;
        }

        function parseSearchOrInput(x) {
            var s = x.split(' ' + "o" + ' '), r = null;
            for (var i in s) { var r2 = parseSearchAndInput(s[i]); if (r == null) { r = r2; } else { for (var j in r2) { if (r.indexOf(r2[j] >= 0)) { r.push(r2[j]); } } } }
            return r;
        }

        function getDevicesThatMatchFilter(x) {
            var negate = x.startsWith('!');
            if (negate) x = x.substring(1);
            var r = [];
            var userSearch = null, ipSearch = null, groupSearch = null, tagSearch = null, agentTagSearch = null, wscSearch = null, osSearch = null, amtSearch = null, descSearch = null, connectivitySearch = null;
            if (x.startsWith('user:'.toLowerCase())) { userSearch = x.substring('user:'.length); }
            else if (x.startsWith('u:'.toLowerCase())) { userSearch = x.substring('u:'.length); }
            else if (x.startsWith('ip:'.toLowerCase())) { ipSearch = x.substring('ip:'.length); }
            else if (x.startsWith('group:'.toLowerCase())) { groupSearch = x.substring('group:'.length); }
            else if (x.startsWith('g:'.toLowerCase())) { groupSearch = x.substring('g:'.length); }
            else if (x.startsWith('tag:'.toLowerCase())) { tagSearch = x.substring('tag:'.length); }
            else if (x.startsWith('t:'.toLowerCase())) { tagSearch = x.substring('t:'.length); }
            else if (x.startsWith('atag:'.toLowerCase())) { agentTagSearch = x.substring('atag:'.length); }
            else if (x.startsWith('a:'.toLowerCase())) { agentTagSearch = x.substring('a:'.length); }
            else if (x.startsWith('os:'.toLowerCase())) { osSearch = x.substring('os:'.length); }
            else if (x.startsWith('amt:'.toLowerCase())) { amtSearch = x.substring('amt:'.length); }
            else if (x.startsWith('desc:'.toLowerCase())) { descSearch = x.substring('desc:'.length); }
            else if (x.startsWith('connectivity:'.toLowerCase())) { connectivitySearch = x.substring('connectivity:'.length); }
            else if (x.startsWith('c:'.toLowerCase())) { connectivitySearch = x.substring('c:'.length); }
            else if (x == 'wsc:ok') { wscSearch = 1; }
            else if (x == 'wsc:noav') { wscSearch = 2; }
            else if (x == 'wsc:noupdate') { wscSearch = 3; }
            else if (x == 'wsc:nofirewall') { wscSearch = 4; }
            else if (x == 'wsc:any') { wscSearch = 5; }

            if (x == '') {
                // No search
                for (var d in nodes) { r.push(d); }
            } else if (ipSearch != null) {
                // IP address search
                for (var d in nodes) { if ((nodes[d].ip != null) && (nodes[d].ip.toLowerCase().indexOf(ipSearch) >= 0)) { r.push(d); } }
            } else if (groupSearch != null) {
                // Group filter
                for (var d in nodes) { if (meshes[nodes[d].meshid].name.toLowerCase().indexOf(groupSearch) >= 0) { r.push(d); } }
            } else if (tagSearch != null) {
                // Tag filter
                for (var d in nodes) {
                    if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(d); }
                    else if (nodes[d].tags != null) { for (var j in nodes[d].tags) { if (nodes[d].tags[j].toLowerCase().indexOf(tagSearch) >= 0) { r.push(d); break; } } }
                }
            } else if (agentTagSearch != null) {
                // Agent Tag filter
                for (var d in nodes) {
                    if ((((nodes[d].agent != null) && (nodes[d].agent.tag == null)) && (agentTagSearch == '')) || ((nodes[d].agent != null) && (nodes[d].agent.tag != null) && (nodes[d].agent.tag.toLowerCase().indexOf(agentTagSearch) >= 0))) { r.push(d); };
                }
            } else if (userSearch != null) {
                // User search
                for (var d in nodes) {
                    if (nodes[d].users && nodes[d].users.length > 0) { for (var i in nodes[d].users) { if (nodes[d].users[i].toLowerCase().indexOf(userSearch) >= 0 || (nodes[d].upnusers && nodes[d].upnusers[i] && nodes[d].upnusers[i].toLowerCase().indexOf(userSearch) >= 0)) { r.push(d); } } }
                }
            } else if (osSearch != null) {
                // OS search
                for (var d in nodes) { if ((nodes[d].osdesc != null) && (nodes[d].osdesc.toLowerCase().indexOf(osSearch) >= 0)) { r.push(d); }; }
            } else if (amtSearch != null) {
                // Intel AMT search
                for (var d in nodes) { if ((nodes[d].intelamt != null) && ((amtSearch == '') || (nodes[d].intelamt.state == amtSearch))) { r.push(d); } }
            } else if (descSearch != null) {
                // Device description search
                for (var d in nodes) { if ((nodes[d].desc != null) && (nodes[d].desc != '') && ((descSearch == '') || (nodes[d].desc.toLowerCase().indexOf(descSearch) >= 0))) { r.push(d); } }
            } else if (wscSearch != null) {
                // Windows Security Center
                for (var d in nodes) {
                    if (nodes[d].wsc) {
                        if ((wscSearch == 1) && (nodes[d].wsc.antiVirus == 'OK') && (nodes[d].wsc.autoUpdate == 'OK') && (nodes[d].wsc.firewall == 'OK')) { r.push(d); }
                        else if (((wscSearch == 2) || (wscSearch == 5)) && (nodes[d].wsc.antiVirus != 'OK')) { r.push(d); }
                        else if (((wscSearch == 3) || (wscSearch == 5)) && (nodes[d].wsc.autoUpdate != 'OK')) { r.push(d); }
                        else if (((wscSearch == 4) || (wscSearch == 5)) && (nodes[d].wsc.firewall != 'OK')) { r.push(d); }
                    }
                }
            } else if (connectivitySearch != null) {
                // Connectivity search
                for (var d in nodes) {
                    if (nodes[d].conn) {
                        if ((nodes[d].conn & 1) != 0) {
                            if (nodes[d].mtype == 4) {
                                if ((nodes[d].porttype == 'PDU') && (connectivitySearch.toLowerCase() == 'switch')) {
                                    r.push(d);
                                } else if (connectivitySearch.toLowerCase() == 'ipkvm') {
                                    r.push(d);
                                }
                            } else if (connectivitySearch.toLowerCase() == 'agent') {
                                r.push(d);
                            }
                        }
                        if (((nodes[d].conn & 2) != 0) && (connectivitySearch.toLowerCase() == 'cira')) { r.push(d); }
                        else if (((nodes[d].conn & 4) != 0) && (connectivitySearch.toLowerCase() == 'amt')) { r.push(d); }
                        if (((nodes[d].conn & 8) != 0) && (connectivitySearch.toLowerCase() == 'relay')) { r.push(d); }
                        if (((nodes[d].conn & 16) != 0) && (connectivitySearch.toLowerCase() == 'mqtt')) { r.push(d); }
                    }
                    if (nodes[d].mtype == 3) {
                        var mesh = meshes[nodes[d].meshid];
                        if (mesh && mesh.relayid && (connectivitySearch.toLowerCase() == 'relay')) {
                            r.push(d);
                        } else if (mesh && (typeof mesh.relayid == 'undefined') && connectivitySearch.toLowerCase() == 'local') {
                            r.push(d);
                        }
                    }
                }
            } else if (x == '*') {
                // Star filter
                for (var d in nodes) { if (stars[nodes[d]._id] == 1) { r.push(d); } }
            } else {
                // Device name search
                try {
                    var rs = x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
                    for (var d in nodes) {
                        if (features2 & 0x00008000) { // Both server and client names must match
                            if (features2 & 0x10000000) {
                                if (rx.test(nodes[d].name.toLowerCase()) || rx.test(meshes[nodes[d].meshid].name.toLowerCase()) || ((nodes[d].rnamel != null) && rx.test(nodes[d].rnamel.toLowerCase()))) { r.push(d); }
                            } else {
                                if (rx.test(nodes[d].name.toLowerCase()) || ((nodes[d].rnamel != null) && rx.test(nodes[d].rnamel.toLowerCase()))) { r.push(d); }
                            }
                        } else {
                            if (features2 & 0x10000000) {
                                if (showRealNames) {
                                    if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase()) || rx.test(meshes[nodes[d].meshid].name.toLowerCase())) { r.push(d); }
                                } else {
                                    if (rx.test(nodes[d].name.toLowerCase()) || rx.test(meshes[nodes[d].meshid].name.toLowerCase())) { r.push(d); }
                                }
                            } else {
                                if (showRealNames) {
                                    if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase())) { r.push(d); }
                                } else {
                                    if (rx.test(nodes[d].name.toLowerCase())) { r.push(d); }
                                }
                            }
                        }
                    }
                } catch (ex) { for (var d in nodes) { r.push(d); } }
            }

            if (negate) { var allIds = []; for (var d in nodes) { allIds.push(d); } return allIds.filter(function(d) { return r.indexOf(d) < 0; }); }
            return r;
        }

        function onSearchInputChanged() {
            var x = Q('SearchInput').value.toLowerCase().trim(); putstore('_search', Q('SearchInput').value);
            x == '' ? QC('SearchInput').remove('search') : QC('SearchInput').add('search');
            //QS('SearchInput')['background-color'] = QS('KvmSearchInput')['background-color'] = (x == '') ? null : '#FDFFBE';
            QV('SearchInputClearButton', (x != ''));
            QV('KvmSearchInputClearButton', (x != ''));

            var r = parseSearchOrInput(x);
            for (var d in nodes) { nodes[d].v = (r.indexOf(d) >= 0) }

            // Check filter dropdown
            var devFilter = Q('DevFilterSelect').value;
            if (devFilter == 1) { for (var d in nodes) { if (((nodes[d].conn == null) || (nodes[d].conn == 0)) && (nodes[d].mtype != 3)) { nodes[d].v = false; } } } // Online, also include Local devices and relayed devices
            if (devFilter == 2) { for (var d in nodes) { var n = nodes[d]; if ((n.sessions == null) || ((n.sessions.kvm == null) && (n.sessions.terminal == null) && (n.sessions.files == null) && (n.sessions.tcp == null) && (n.sessions.udp == null))) { n.v = false; } } } // Sessions
            if (devFilter == 3) { for (var d in nodes) { if (stars[nodes[d]._id] != 1) { nodes[d].v = false; } } } // Starred
            if (devFilter == 4) { for (var d in nodes) { if (nodes[d].intelamt == null) { nodes[d].v = false; } } } // Intel AMT
            if (devFilter == 5) { for (var d in nodes) { if ((nodes[d].conn != null) && (nodes[d].conn != 0)) { nodes[d].v = false; } } } // Offline
            if (devFilter == 6) { for (var d in nodes) { var n = nodes[d]; if ((n.sessions == null) || (n.sessions.help == null)) { n.v = false; } } } // Sessions
            if (devFilter == 7) { for (var d in nodes) { var n = nodes[d]; if ((n.tags == null) || (n.tags.length == 0)) { n.v = false; } } } // Tagged
            if (devFilter == 8) { for (var d in nodes) { var n = nodes[d]; if ((n.tags != null) && (n.tags.length > 0)) { n.v = false; } } } // Untagged
        }

        function newContextMenu(event, cmenu) {
            switch (cmenu) {
                case 1: { // Terminal connect button context menu
                    if ((currentNode == null) || (currentNode.agent == null) || (currentNode.mtype == 3)) return true;
                    // If the server has a specific terminal shell mode, don't show connection options
                    if (serverinfo.linuxshell && (currentNode.agent.id > 4)) return;
                    var nextElement = event.target.nextElementSibling;
                    if (nextElement) {
                        var children = nextElement.children;  // This will get only element nodes
                        var displayClass = (currentNode.agent.id > 4 && !isWindowsNode(currentNode)) ? 'lin' : (currentNode.agent.id == 34) ? 'add' : null;
                        for (var i = 0; i < children.length; i++) {
                            if (displayClass) {
                                children[i].style.display = children[i].classList.contains(displayClass) ? 'block' : 'none';
                            } else {
                                children[i].style.display = (children[i].classList.contains('lin') || children[i].classList.contains('ass')) ? 'none' : 'block';
                            }
                        }
                    }
                    break;
                }
            }
        }

        var contextelement = null;
        function handleContextMenu(event) {
            // When called, we look for elements with "cmenu=xxx" and show the right context menu for that element.
            hideContextMenu();
            if (xxdialogMode) return;
            var scrollLeft = (window.pageXOffset !== null) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft;
            var scrollTop = (window.pageYOffset !== null) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
            var elem = document.elementFromPoint(event.pageX - scrollLeft, event.pageY - scrollTop);
            while (elem && elem != null && elem.attributes.cmenu == null) { elem = elem.parentElement; } // Go up until element with context menu or root is reached.
            if (elem == null) return true; // No "cmenu=xxx" found at the element that was clicked.
            var cmenu = elem.attributes.cmenu.value;

            switch (cmenu) {
                case 'devsContentMenu': {
                    // Device content menu
                    contextelement = elem;
                    var contextmenudiv = document.getElementById('contextMenu');
                    showContextMenuDiv(contextmenudiv, event.pageX, event.pageY);

                    // Get the node and set the menu options
                    var nodeid;
                    if (Q('viewselect').value == 1) {
                        nodeid = contextelement.children[0].children[0].children[1].children[0].attributes.onmouseup.value;
                    } else {
                        nodeid = contextelement.children[1].attributes.onmouseup.value;
                    }
                    var node = getNodeFromId(nodeid.substring(12, nodeid.length - 18));
                    var rights = GetNodeRights(node);
                    var consoleRights = ((rights & 16) != 0);

                    // Check if we have desktop, terminal and file access
                    var desktopAccess = ((rights == 0xFFFFFFFF) || ((rights & 65536) == 0));
                    var terminalAccess = ((rights == 0xFFFFFFFF) || ((rights & 512) == 0));
                    var fileAccess = ((rights == 0xFFFFFFFF) || ((rights & 1024) == 0));

                    QV('cxdesktop', ((node.conn & 1) != 0) && (node.mtype != 4) && ((node.mtype == 1) || (node.agent == null) || (node.agent.caps == null) || ((node.agent.caps & 1) != 0) || (node.intelamt && (node.intelamt.state == 2))) && ((rights & 8) || (rights & 256)) && desktopAccess);
                    QV('cxterminal', ((node.conn & 1) != 0) && (node.mtype != 4) && ((node.mtype == 1) || (node.agent == null) || (node.agent.caps == null) || ((node.agent.caps & 2) != 0) || (node.intelamt && (node.intelamt.state == 2))) && (rights & 8) && terminalAccess);
                    QV('cxfiles', (node.mtype != 4) && ((node.mtype == 2) && ((node.agent == null) || (node.agent.caps == null) || ((node.agent.caps & 4) != 0))) && (rights & 8) && fileAccess);
                    QV('cxevents', (node.intelamt != null) && ((node.intelamt.state == 2) || (node.conn & 2)) && (rights & 8));
                    QV('cxdetails', node.mtype < 3);
                    QV('cxwebvnc', ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((rights & 8) != 0) && ((features & 0x20000000) == 0)));
                    QV('cxwebrdp', ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((rights & 8) != 0) && ((features & 0x40000000) == 0)));
                    QV('cxwebssh', ((features2 & 0x200) && (((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((rights & 8) != 0)));
                    QV('cxconsole', (consoleRights && (node.mtype == 2) && ((node.agent == null) || (node.agent.caps == null) || ((node.agent.caps & 8) != 0))) && (rights & 8));
                    QV('cxrdp', false); // always have the RDP hidden
                    if ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((rights & 8) != 0)) {
                        if ((node.agent.id > 0) && (node.agent.id < 5)) {
                            if (navigator.platform.toLowerCase() == 'win32') {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
                                    QV('cxrdp', true);
                                }
                            }
                        }
                    }
                    QV('cxmgroupsplit', true);
                    QV('cxstar', true);
                    break;
                }
                default: {
                    // Basic context menu
                    if ((contextmenudiv = document.getElementById(cmenu)) == null) return true;
                    contextelement = elem;
                    showContextMenuDiv(contextmenudiv, event.pageX, event.pageY);
                    break;
                }
            }
            return haltEvent(event);
        }

        function showContextMenuDiv(element, x, y) {
            var clientRect = document.documentElement.getBoundingClientRect();
            var docHeight = clientRect.height;
            var docWidth = clientRect.width;
            element.style.left = element.style.right = element.style.top = element.style.bottom = null;
            if (x > (docWidth / 2)) { element.style.right = (docWidth - event.pageX) + 'px'; } else { element.style.left = event.pageX + 'px'; }
            if (y > (docHeight / 2)) { element.style.bottom = (docHeight - event.pageY) + 'px'; } else { element.style.top = event.pageY + 'px'; }
            element.style.display = 'block';
        }

        function cmaction(action, event) {
            var nodeid;
            if (Q('viewselect').value == 1) {
                nodeid = contextelement.children[0].children[0].children[1].children[0].attributes.onmouseup.value;
            } else {
                nodeid = contextelement.children[1].attributes.onmouseup.value;
            }
            nodeid = nodeid.substring(12, nodeid.length - 18);
            if (action == 9) { Q('viewselect').value = 3; Q('viewselect').onchange(); Q('autoConnectDesktopCheckbox').checked = true; Q('autoConnectDesktopCheckbox').onclick(); } // Multi-Desktop
            if ((action > 0) && (action < 9)) {
                var panel = [0, 10, 12, 11, 13, 16, 17, 15, 19][action]; // (invalid), General, Desktop, Terminal, Files, Events, Console, Plugin
                if (event && (event.shiftKey == true)) {
                    // Open the device in a different tab
                    safeNewWindow(window.location.origin + '?node=' + nodeid.split('/')[2] + '&viewmode=' + panel + '&hide=16' + ((urlargs.key) ? ('&key=' + urlargs.key) : ''), 'meshcentral:' + nodeid);
                } else {
                    // Go to the right panel
                    gotoDevice(nodeid, panel);

                    // If possible, connect...
                    var mesh = meshes[currentNode.meshid];
                    if ((currentNode.conn & 1) && (mesh.mtype == 2)) {
                        if ((panel == 11) && (desktop == null) && (currentNode.agent.caps & 1)) { connectDesktop(null, 3); } // Desktop
                        if ((panel == 12) && (terminal == null) && (currentNode.agent.caps & 2)) { connectTerminal(null, 1); } // Terminal
                        if ((panel == 13) && (files == null)) { connectFiles(null); } // files
                    }
                }
            }
            if (action == 10) {
                // Toggle star
                var elements = document.getElementsByClassName('DeviceCheckbox'), selectedDevices = [];
                for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { selectedDevices.push(elements[i].defaultValue.substring(6)); } }
                if (selectedDevices.length == 0) { selectedDevices.push(nodeid); }
                for (var i in selectedDevices) { if (stars[selectedDevices[i]] != null) { delete stars[selectedDevices[i]]; delete selectedDevices[i]; } }
                var starcount = Object.keys(stars).length;
                for (var i in selectedDevices) { if ((starcount < 200) && (stars[selectedDevices[i]] == null)) { stars[selectedDevices[i]] = 1; starcount++; } }
                putstore('stars', JSON.stringify(stars));
                updateDeviceViewDevice(nodeid);
                if (Q('DevFilterSelect').value == 3) { mainUpdate(1); }
            }
            else if (action == 11) { p10mstsc(nodeid); } // Web-RDP
            else if (action == 12) { p10rfb(nodeid); } // Web-VNC
            else if (action == 13) { p10ssh(nodeid); } // Web-SSH
            else if (action == 14) { p10MCRouter(nodeid, 3); } // RDP
        }

        function cmmeshaction(action) {
            var meshid = contextelement.attributes.onclick.value.substring(10, contextelement.attributes.onclick.value.length - 2);
            var elements = document.getElementsByClassName('DeviceCheckbox');
            if ((action == 1) || (action == 2)) {
                // Change the visible check boxes
                for (var i = 0; i < elements.length; i++) {
                    if ((elements[i].attributes) && (elements[i].attributes['class']['value'].split(' ')[0] == meshid)) { elements[i].checked = (action == 1); }
                }
                // Update the list of checked nodes
                if (action == 1) {
                    for (var i in nodes) { if ((nodes[i].meshid == meshid) && (nodes[i].v == true)) { checkedNodeids[nodes[i]._id] = 1; } }
                } else if (action == 2) {
                    for (var i in checkedNodeids) {
                        var n = getNodeFromId(i);
                        if ((n != null) && (n.meshid == meshid) && (n.v == true)) { delete checkedNodeids[i]; }
                    }
                }
            }
            //if (action == 3) { window.location = "multidesktop.aspx?mesh=" + meshid + "&auto=1"; }
            p1updateInfo();
        }

        function cmconnectfilesaction() {
            connectFiles(null, 1, 0x0020);
        }

        function cmtermaction(action, consent) {
            if (action < 100) {
                connectTerminal(null, 1, { protocol: action, consent: consent });
            } else if (action == 100) {
                connectTerminal(null, 1, { protocol: 1, requireLogin: true, consent: consent });
            }
        }

        function cmdeskaction(action) {
            if (action == 1) { connectDesktop(null, 3, null, 0x0008 + 0x0040); } // Consent Prompt + Privacy bar
            if (action == 2) { connectDesktop(null, 3, null, 0x0008); } // Consent Prompt
            if (action == 3) { connectDesktop(null, 3, null, 0x0040); } // Privacy bar
            if (action == 10) { if (desktop != null) { desktop.sendCtrlMsg('{"ctrlChannel":"102938","type":"autolock","value":true}'); connectDesktop(); } } // Disconnect and lock
            if (action == 11) { if (desktop != null) { desktop.sendCtrlMsg('{"ctrlChannel":"102938","type":"autolock","value":false}'); connectDesktop(); } } // Disconnect without lock
        }

        function cmaltportaction(action) {
            if (xxdialogMode) return;
            var x = "Puerto de conexión remoto RDP:" + '<br /><br /><input type=text placeholder="3389" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10rdpport type=text>';
            setModalContent('xxAddAgent', "Conexión RDP", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                // Save the new RDP port to the server
                var rdpport = ((Q('d10rdpport').value.length > 0) ? parseInt(Q('d10rdpport').value) : 3389);
                meshserver.send({ action: 'changedevice', nodeid: currentNode._id, rdpport: rdpport });
                //if (currentNode != null) { p10MCRouter(currentNode._id, 3, rdpport); }
            });
            Q('d10rdpport').focus();
            if (currentNode.rdpport != null) { Q('d10rdpport').value = currentNode.rdpport; }
        }

        function cmsshportaction(action) {
            if (xxdialogMode) return;
            var x = "Puerto de conexión remota SSH:" + '<br /><br /><input type=text placeholder="22" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10sshport type=text>';
            setModalContent('xxAddAgent', "Conexión SSH", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                // Save the new SSH port to the server
                var sshport = ((Q('d10sshport').value.length > 0) ? parseInt(Q('d10sshport').value) : 22);
                meshserver.send({ action: 'changedevice', nodeid: currentNode._id, sshport: sshport });
                //if (currentNode != null) { p10MCRouter(currentNode._id, 3, sshport); }
            });
            Q('d10sshport').focus();
            if (currentNode.sshport != null) { Q('d10sshport').value = currentNode.sshport; }
        }

        function cmrfbportaction(action) {
            if (xxdialogMode) return;
            var x = "Puerto de conexión remota VNC:" + '<br /><br /><input type=text placeholder="5900" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10rfbport type=text>';
            setModalContent('xxAddAgent', "Conexión VNC", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                // Save the new RFB port to the server
                var rfbport = ((Q('d10rfbport').value.length > 0) ? parseInt(Q('d10rfbport').value) : 3389);
                meshserver.send({ action: 'changedevice', nodeid: currentNode._id, rfbport: rfbport });
                //if (currentNode != null) { p10rfb(currentNode._id, rfbport); }
            });
            Q('d10rfbport').focus();
            if (currentNode.rfbport != null) { Q('d10rfbport').value = currentNode.rfbport; }
        }

        function cmhttpportaction(action) {
            if (xxdialogMode) return;
            var x = "Puerto de conexión remota HTTP:" + '<br /><br /><input type=text placeholder="80" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10httpport type=text>';
            setModalContent('xxAddAgent', "Conexión HTTP", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                // Save the new HTTP port to the server
                var httpport = ((Q('d10httpport').value.length > 0) ? parseInt(Q('d10httpport').value) : 80);
                meshserver.send({ action: 'changedevice', nodeid: currentNode._id, httpport: httpport });
                //if (currentNode != null) { p10rfb(currentNode._id, httpport); }
            });
            Q('d10httpport').focus();
            if (currentNode.httpport != null) { Q('d10httpport').value = currentNode.httpport; }
        }

        function cmhttpsportaction(action) {
            if (xxdialogMode) return;
            var x = "Puerto de conexión remota HTTPS:" + '<br /><br /><input type=text placeholder="443" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10httpsport type=text>';
            setModalContent('xxAddAgent', "Conexión HTTPS", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                // Save the new HTTP port to the server
                var httpsport = ((Q('d10httpsport').value.length > 0) ? parseInt(Q('d10httpsport').value) : 443);
                meshserver.send({ action: 'changedevice', nodeid: currentNode._id, httpsport: httpsport });
                //if (currentNode != null) { p10rfb(currentNode._id, httpsport); }
            });
            Q('d10httpsport').focus();
            if (currentNode.httpsport != null) { Q('d10httpsport').value = currentNode.httpsport; }
        }

        function cmfilesaction(action) {
            if (xxdialogMode) return;
            var filetreexx = p13sort_files(p13filetree.dir);
            var file = filetreexx[parseInt(contextelement.attributes.fileindex.nodeValue)];
            if (action == 1) { // Rename the file
                setModalContent('xxAddAgent', "Renombrar", '<input type=text id=p13renameinput maxlength=64 onkeyup=p13fileNameCheck(event) style=width:100% value="' + file.n + '" />');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13renamefileEx(3, { action: 'rename', path: p13filetreelocation.join('/'), oldname: file.n }));
                focusTextBox('p13renameinput');
                p13fileNameCheck();
            } else if (action == 2) { // Edit the file
                if (file.s <= 204800) {
                    p13downloadfile(encodeURIComponentEx(p13filetreelocation.join('/') + '/' + file.n), encodeURIComponentEx(file.n), file.s, 'viewer');
                } else { messagebox("Editor de Archivos", "Solo se pueden editar archivos de menos de 200k."); }
            } else if (action == 3) {
                // Delete the file
                setModalContent('xxAddAgent', "Eliminar", "¿Eliminar elemento?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13deletefileCm(3, file));
            }
        }

        function cmexpandaction(action) {
            if (action == 1) {
                CollapsedGroups = {};
            } else {
                if (sort == 0) { CollapsedGroups = {}; for (var i in meshes) { CollapsedGroups[i] = true; } for (var i in nodes) { if (meshes[nodes[i].meshid] == null) { CollapsedGroups[nodes[i].meshid] = true; } } }
                if (sort == 1) { CollapsedGroups = {}; for (var i = 0; i < 10; i++) { CollapsedGroups['pwr:' + i] = true; } }
                if (sort == 3) { CollapsedGroups = {}; for (var i in nodes) { var node = nodes[i]; if (node.tags) { for (var j in node.tags) { CollapsedGroups['tag:' + encodeURIComponentEx(node.tags[j])] = true; } } } }
                if (sort == 4) {
                    CollapsedGroups = {};
                    for (var i in nodes) {
                        var node = nodes[i];
                        var mesh2 = meshes[node.meshid];
                        if (mesh2) {
                            CollapsedGroups['tag:' + encodeURIComponentEx(mesh2.name)] = true;
                            if (node.tags) { for (var j in node.tags) { CollapsedGroups['tag:' + encodeURIComponentEx(mesh2.name + ' - ' + node.tags[j])] = true; } }
                        } else {
                            if (node.tags) { for (var j in node.tags) { CollapsedGroups['tag:' + encodeURIComponentEx('**INDV*~*DEVS** - ' + node.tags[j])] = true; } }
                        }
                    }
                }
            }
            putstore('_collapse', JSON.stringify(CollapsedGroups));
            updateCollapseAllButton();
            mainUpdate(4);
        }

        function cmdeskplayeraction(action) {
            if (xxdialogMode) return;
            safeNewWindow(window.location.origin + '{{{domainurl}}}player.htm' + ((urlargs.key) ? ('?key=' + urlargs.key) : ''), 'meshcentral-deskplayer');
        }

        function cmdeskshortcutaction(action) {
            if (xxdialogMode) return;
            deskCustomizeKeys();
        }

        function cmdeskpreconfigtypeaction(action) {
            if (xxdialogMode) return;
            if (action == -1) { deskCustomizeStrings(); }
            else if (action < 1000) { showDeskTypeEx(serverinfo.preConfiguredRemoteInput[action].value); } // Type server pre-configured input string
            else { showDeskTypeEx(deskKeyboardStrings[action - 1000].v); } // Type user pre-configured input string
        }

        function cmdeskpreconfigscriptaction(action) {
            meshserver.send({ action: 'runcommands', nodeids: [currentNode._id], presetcmd: action });
        }

        function p13deletefileCm(b, file) {
            files.sendText({ action: 'rm', reqid: 1, path: p13filetreelocation.join('/'), delfiles: [file.n], rec: false });
            p13folderup(999);
        }

        /*
        function pluginTabClose() {
            var pluginTab = contextelement;
            var pname = pluginTab.getAttribute('x-data-plugin-sname');
            var pdiv = Q('plugin-'+pname);
            pdiv.parentNode.removeChild(pdiv);
            pluginTab.parentNode.removeChild(pluginTab);
            QV('p42', true);
            goPlugin(-1);
        }
        */

        function hideContextMenu() {
            QV('contextMenu', false);
            QV('meshContextMenu', false);
            QV('altPortContextMenu', false);
            QV('rfbPortContextMenu', false);
            QV('sshPortContextMenu', false);
            QV('httpPortContextMenu', false);
            QV('httpsPortContextMenu', false);
            QV('filesContextMenu', false);
            QV('deskPlayerContextMenu', false);
            QV('deskKeyShortcutContextMenu', false);
            QV('userDropdownMenu', false);
            QV('deskPreConfigShortcutContextMenu', false);
            QV('deskPreConfigScriptContextMenu', false);
            QV('expandAllContextMenu', false);
            //QV('pluginTabContextMenu', false);
            contextelement = null;
        }

        //
        // DEVICES MAP
        //

        // Maps code starts from here. Initialize all the variables
        var xxmap = {
            map: null,
            contextmenu: null,
            activeInteractions: [], // Save Modified features in this list
            showindex: 0,
            markersSource: null, // Initialize a Source Vector
            markersLayer: null,
            mapLayer: null, // Create a tile and use OSM source
            mapView: null, // Sets the initial view
        }

        // Add a feature for every Node and change style if connection status changes
        function updateMapMarkers(selectedMesh) {
            if ((features & 0x00008000) == 0) return;
            if ((xxmap != null) && (xxmap.map == null)) { try { loadmap(); } catch (ex) { console.error('loadmap() exception', ex); } }
            if (xxmap == null) return;
            var boundingBox = null;
            for (var i in nodes) {
                try {
                    var loc = map_parseNodeLoc(nodes[i]), feature = xxmap.markersSource.getFeatureById(nodes[i]._id);
                    if ((loc != null) && ((nodes[i].meshid == selectedMesh) || (selectedMesh == null))) { // Draw markers for devices with locations
                        var lat = loc[0], lon = loc[1], type = loc[2];
                        if (boundingBox == null) { boundingBox = [lat, lon, lat, lon, 0]; } else { if (lat < boundingBox[0]) { boundingBox[0] = lat; } if (lon < boundingBox[1]) { boundingBox[1] = lon; } if (lat > boundingBox[2]) { boundingBox[2] = lat; } if (lon > boundingBox[3]) { boundingBox[3] = lon; } }
                        if (feature == null) { addFeature(nodes[i]); boundingBox[4] = 1; } else { updateFeature(nodes[i], feature); feature.setStyle(markerStyle(nodes[i], loc[2])); } // Update Feature
                    } else {
                        if (feature) { xxmap.markersSource.removeFeature(feature); }
                    }
                } catch (ex) { console.error('updateMapMarkers() exception', ex, JSON.stringify(nodes[i])); }
            }
            return boundingBox;
        }

        var map_cm_popup, map_cm_editMarker, map_cm_clearMarker, map_cm_saveMarker, map_cm_nodemenu_items, contextmenu_items;
        if (features & 0x00008000) {
            // Show node details on hovering over a feature
            map_cm_popup = new ol.Overlay({ element: Q('xmap-info-window'), positioning: 'bottom-center', stopEvent: false });

            // Edit Marker item
            map_cm_editMarker = { text: "Modificar ubicación del nodo", callback: function (obj) { modifyMarkerloc(obj.data); } };

            // Clear Marker item
            map_cm_clearMarker = {
                text: "Remover ubicación del nodo", callback: function (obj) {
                    meshserver.send({ action: 'changedevice', nodeid: obj.data.a, userloc: [] }); // Clear the user position marker
                }
            };

            // Save Marker item
            map_cm_saveMarker = { text: "Guardar ubicación del nodo", callback: function (obj) { saveMarkerloc(obj.data); } };

            // Build a context menu for a feature
            map_cm_nodemenu_items = [
                { text: "Información general", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 10); } } },
                { text: "Escritorio", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 11); } } },
                { text: "Terminal", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 12); } } },
                { text: "Intel&reg; AMT", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 14); } } },
                '-',
                { text: "Acercar a medida", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 19); } },
                { text: "Alejar a medida", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 2); } }
            ];

            // Context menu for clicks other than on feature
            contextmenu_items = [
                { text: "Actualizar", callback: function () { refreshMap(true, true); } },
                { text: "Zoom para adaptarse a la extensión", callback: function () { zoomToFitExtent(); } },
                { text: "Centrar mapa aquí", callback: function (obj) { xxmap.mapView.animate({ center: obj.coordinate }); } },
                { text: "Coloca el nodo aquí", callback: function (obj) { placeNode(obj.coordinate); } }
            ];
        }

        function stringToIntHash(str) {
            var hash = 0, i;
            for (i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; }
            return hash;
        };

        // Get the lat/lon from a node
        function map_parseNodeLoc(node) {
            var loc = null, t = 0;
            if (node.iploc) { loc = node.iploc; t = 1; }
            if (node.wifiloc) { loc = node.wifiloc; t = 2; }
            if (node.gpsloc) { loc = node.gpsloc; t = 3; }
            if (node.userloc) { loc = node.userloc; t = 4; }
            if ((loc == null) || (typeof loc != 'string')) return null;
            loc = loc.split(',');
            if (t == 1) {
                // If this is IP location, randomize the position a little.
                return [parseFloat(loc[0]) + (stringToIntHash(node._id.substring(0, 20)) / 100000000000), parseFloat(loc[1]) + (stringToIntHash(node._id.substring(20)) / 100000000000), t];
            } else {
                // Return the real position
                return [parseFloat(loc[0]), parseFloat(loc[1]), t];
            }
        }

        // Load the entire map
        function loadmap() {
            if ((features & 0x00008000) == 0) return;
            if (xxmap == null) return;
            if ((features & 0x8000) == 0) { xxmap = null; return; } // Geolocation not supported
            QV('viewselectmapoption', true);
            QV('devViewButton4', true);
            try {
                // Initialize a Source Vector
                xxmap.markersSource = new ol.source.Vector();

                xxmap.markersLayer = new ol.layer.Vector({
                    source: xxmap.markersSource
                });

                // Create a tile and use OSM source
                xxmap.mapLayer = new ol.layer.Tile({
                    source: new ol.source.OSM({
                        tileLoadFunction: function(imageTile, src) {
                            var img = imageTile.getImage();
                            img.referrerPolicy = 'origin';
                            img.src = src;
                        }
                    })
                });

                xxmap.mapView = new ol.View({ // Set the initial view
                    center: ol.proj.transform([0, 0], 'EPSG:4326', 'EPSG:3857'),
                    zoom: 2,
                    minZoom: 2,
                    maxZoom: 20,
                    extent: ol.proj.transformExtent([-100000, -69.55, 100000, 69.55], 'EPSG:4326', 'EPSG:3857')
                });

                xxmap.map = new ol.Map({
                    target: 'xdevicesmap',
                    layers: [xxmap.mapLayer, xxmap.markersLayer],
                    view: xxmap.mapView
                });

                xxmap.map.addOverlay(map_cm_popup);

                // Goto information tab if a user clicks on a feature
                xxmap.map.on('click', function (evt) {
                    var feature = xxmap.map.forEachFeatureAtPixel(evt.pixel, function (feat, layer) { return feat; });
                    if (feature) {
                        var nodeid = feature.getId();
                        if (nodeid != null) { gotoDevice(nodeid, 10); } // Goto general info tab
                        else { // For pointer
                            var nodeFeatgoto = getCorrespondingFeature(feature); gotoDevice(nodeFeatgoto.getId(), 10);
                        }
                    }
                });

                // On hover feature show the name of the node. Also add pointer style
                xxmap.map.on('pointermove', function (evt) {
                    var feature = xxmap.map.forEachFeatureAtPixel(evt.pixel, function (feat, layer) { return feat; });
                    if (feature) {
                        xxmap.map.getTargetElement().style.cursor = 'pointer';
                        var coord = feature.getGeometry().getCoordinates();
                        // map_cm_popup.setPosition(evt.coordinate);
                        map_cm_popup.setPosition(coord);
                        var featid = feature.getId();
                        if (featid) {
                            QH('xmap-info-window', feature.get('name'));
                        } else {
                            var nodeFeat = getCorrespondingFeature(feature); // Return the node feature associated to pointer.
                            QH('xmap-info-window', nodeFeat.get('name'));
                        }
                    } else {
                        xxmap.map.getTargetElement().style.cursor = '';
                        QH('xmap-info-window', '');
                    }
                });

                // Initialize context menu for openlayers
                var contextmenu = new ContextMenu({
                    width: 160,
                    defaultItems: false, // defaultItems are Zoom In/Zoom Out
                    items: contextmenu_items
                });

                // On right click open the context menu
                contextmenu.on('open', function (evt) {
                    var feature = xxmap.map.forEachFeatureAtPixel(evt.pixel, function (ft, l) { return ft; });
                    xxmap.contextmenu.clear(); //Clear the context menu
                    if (feature) {
                        var featId = feature.getId();
                        if (featId) { addContextMenuItems(feature); } // Node feature will have an id
                        else { // If the feature is a pointer, Get its corresponding Node feature
                            var nodeFeature = getCorrespondingFeature(feature); //return the node feature associated to pointer.
                            if (nodeFeature) { addContextMenuItems(nodeFeature); }
                            else { xxmap.contextmenu.extend(contextmenu_items); }
                        }
                    }
                    else { xxmap.contextmenu.extend(contextmenu_items); }
                });
                if (xxmap.contextmenu == null) { xxmap.contextmenu = contextmenu; }
                xxmap.map.addControl(xxmap.contextmenu);
                //addMeshOptions(); // Adds Mesh names to mesh dropdown
            } catch (ex) {
                console.log(ex);
                QV('viewselectmapoption', false);
                QV('devViewButton4', false);
                xxmap = null;
            }
        }

        // Add feature on to Map for a Node
        function addFeature(node, lat, lon) {
            var existingfeature = getModifiedFeature(node._id); // Check if Corresponding feature was Modified ( Modifed feature are in active interactions list)
            if (existingfeature) { xxmap.markersSource.addFeature(existingfeature); } // Add that existing feature
            else { // Add new feature for this node
                if (!lat && !lon) { var loc = map_parseNodeLoc(node); lat = loc[0]; lon = loc[1]; }

                // Fix the longiture and send an event to patch the db to correct coordinate format. It will cause second unnecessary updateFeature on this node to the map.
                if (lon > 180) { lon = 180 - lon; meshserver.send({ action: 'changedevice', nodeid: node._id, userloc: [lat, lon] }); }

                if ((lat < 90) && (lat > -90) && (lon < 180) && (lon > -180)) { // Check valid lat/lon
                    var feature = new ol.Feature({ geometry: new ol.geom.Point(ol.proj.transform([lon, lat], 'EPSG:4326', 'EPSG:3857')), name: node.name, status: node.conn, lat: lat, lon: lon });
                    feature.setId(node._id); // Set id for the device as nodeid
                    feature.setStyle(markerStyle(node));
                    xxmap.markersSource.addFeature(feature); // Add the feature to Marker Source
                }
            }
        }

        // Removing any feature from map
        function removeFeature(node) {
            var feature = xxmap.markersSource.getFeatureById(node._id);
            if (feature) { xxmap.markersSource.removeFeature(feature); }
        }

        // Update feature
        function updateFeature(node, feature) {
            if (node.conn != feature.get('status')) { // Update status if changed
                feature.set('status', node.conn)
                feature.setStyle(markerStyle(node));
            }

            // Since this is IP address location, add some fixed randomness to the location. Avoid pin pile-up.
            var loc = map_parseNodeLoc(node);
            if (loc != null) {
                var lat = loc[0], lon = loc[1];
                if ((lat != feature.get('lat')) || (lon != feature.get('lon'))) { // Update lat and lon if changed
                    feature.set('lat', lat); feature.set('lon', lon);
                    var modifiedCoordinates = ol.proj.transform([parseFloat(lon), parseFloat(lat)], 'EPSG:4326', 'EPSG:3857');
                    feature.getGeometry().setCoordinates(modifiedCoordinates);
                }
            }

            if (node.name != feature.get('name')) { feature.set('name', node.name); } // Update name
        }

        // Enable dragging of a marker after edit option is clicked in context menu
        function modifyMarkerloc(ft) {
            var featid = ft.getId();
            if (featid) {
                ft.setStyle(markerStyle(getNodeFromId(ft.a), 4)); // Switch to a user marker
                if (!getActiveInteractions(ft)) {
                    var dragInteration = new ol.interaction.Modify({
                        features: new ol.Collection([ft]),
                        pixelTolerance: 10
                    });
                    xxmap.activeInteractions.push({ featureid: featid, feature: ft, interaction: dragInteration }); // Also keep track of Interactions
                    xxmap.map.addInteraction(dragInteration);
                }
            }
        }

        // This will be called when save location option is clicked in context menu
        function saveMarkerloc(ft) {
            var featid = ft.getId()
            if (featid) {
                var actInteraction = getActiveInteractions(ft);
                if (actInteraction) { // Check if the interaction exists
                    xxmap.map.removeInteraction(actInteraction); //Clear Interaction for that node
                    removeInteraction(featid);
                    var coord = ft.getGeometry().getCoordinates();
                    var v = ol.proj.transform(coord, 'EPSG:3857', 'EPSG:4326');
                    if (v[0] > 180) { v[0] = 180 - v[0]; }
                    var vx = [v[1], v[0]]; // Flip the coordinates around, lat/long
                    meshserver.send({ action: 'changedevice', nodeid: featid, userloc: vx }); // Send them to server to save changes
                }
            }
        }

        // Style the Markers
        function markerStyle(node, type) {
            if (type == null) {
                type = 0;
                if (node.iploc) { type = 1; }
                if (node.wifiloc) { type = 2; }
                if (node.gpsloc) { type = 3; }
                if (node.userloc) { type = 4; }
            }
            var types = ['', '-ip', '-wifi', '-gps', '-user'];
            var color = connStateColor(node);
            var style = new ol.style.Style({
                image: new ol.style.Icon({ color: color, anchor: [0.5, 1], src: 'images/mapmarker' + types[type] + '.png' })
                //stroke: new ol.style.Stroke({ color: '#000', width: 20 })
                //text: new ol.style.Text({ text: 'bob!', textAlign: 'right', offsetX: -10, fill: new ol.style.Fill({ color: '#000' }), stroke: new ol.style.Stroke({ color: '#fff', width: 2 }) })
            });

            //deviceMark.setStyle(new ol.style.Style({
            //    text: new ol.style.Text({
            //        //font: '12px helvetica,sans-serif',
            //        text: currentNode.name,
            //        textAlign: 'right',
            //        offsetX: -10,
            //        fill: new ol.style.Fill({ color: '#000' }),
            //        stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
            //        }),
            //    image: new ol.style.Icon(({ color: [113, 140, 0], src: 'images/dot.png' })) }));

            return [style];
        }

        // TODO: Add more connection status types. Currently we only change color if connection status changes
        function connStateColor(nodeConn) {
            if (nodeConn.conn == 1 || nodeConn.conn == 3 || nodeConn.conn == 5) { return '#00ffdd'; } // Green for connected devices
            return '#C70039'; // Red if the Agent is not connected
        }

        // Add save/edit option to context menu
        function addContextMenuItems(feature) {
            if (getActiveInteractions(feature)) { // If this feature is modified then display save option in contextmenu
                map_cm_saveMarker.data = feature;
                xxmap.contextmenu.push(map_cm_saveMarker);
            } else {
                map_cm_editMarker.data = feature;
                xxmap.contextmenu.push(map_cm_editMarker);
                var node = getNodeFromId(feature.a);
                if (node.userloc) {
                    map_cm_clearMarker.data = feature;
                    xxmap.contextmenu.push(map_cm_clearMarker);
                }
            }
            map_cm_nodemenu_items.forEach(function (item) {
                if (item.text == "Acercar a medida" || item.text == "Alejar a medida") { item.data = feature; }
                else { if (item != '-') { item.data = feature.getId(); } }
            });
            xxmap.contextmenu.extend(map_cm_nodemenu_items);
        }

        // Return a active Interaction if it exists in activeInteractions list
        function getActiveInteractions(feature) {
            var featid = feature.getId();
            for (var i = 0; i < xxmap.activeInteractions.length; i++) {
                if (xxmap.activeInteractions[i].featureid == featid) { return xxmap.activeInteractions[i].interaction; }
            }
            return false;
        }

        // Return Modified feature based on Id
        function getModifiedFeature(featid) {
            if (featid) {
                for (var i = 0; i < xxmap.activeInteractions.length; i++) {
                    if (xxmap.activeInteractions[i].featureid == featid) { return xxmap.activeInteractions[i].feature; }
                }
            }
            return null;
        }

        // Remove Interaction
        function removeInteraction(ftid) {
            var index = -1;
            for (var i = 0; i < xxmap.activeInteractions.length; i++) {
                if (xxmap.activeInteractions[i].featureid === ftid) { index = i; break; }
            }
            if (index >= 0) { xxmap.activeInteractions.splice(index, 1); }
        }

        // Check if pointer coordinates are equal to features and return node feature
        function getCorrespondingFeature(pointerFeat) {
            var pointerCoord = pointerFeat.getGeometry().getCoordinates();
            for (var i = 0; i < xxmap.activeInteractions.length; i++) {
                var modifiedFeatures = xxmap.activeInteractions[i].feature;
                var fearCoord = modifiedFeatures.getGeometry().getCoordinates();
                if (fearCoord[0].toFixed(5) == pointerCoord[0].toFixed(5) && fearCoord[1].toFixed(5) == pointerCoord[1].toFixed(5)) { return modifiedFeatures; }
            }
            return null;
        }

        // Refresh the map and clear list
        function refreshMap(reset, rebound) {
            if (reset) {
                xxmap.map.setTarget(null);
                xxmap.map = null;
                xxmap.markersSource = null;
                xxmap.mapView = null;
                xxmap.mapLayer = null;
                xxmap.activeInteractions = []; // Clear Active Interaction list
            }
            //clearMeshOptions();
            //onSelectMeshChange();
            var box = updateMapMarkers();
            if ((box != null) && (rebound || (box[4] == 1))) {
                var clat = (box[0] + box[2]) / 2;
                var clon = (box[1] + box[3]) / 2;
                var cscale = Math.max(Math.abs(box[0] - box[2]), Math.abs(box[1] - box[3]));
                var view = xxmap.map.getView();
                view.setCenter(ol.proj.transform([clon, clat], 'EPSG:4326', 'EPSG:3857'));
                var i = 360, j = -2;
                while (i > cscale) { j++; i = i / 2; }
                view.setZoom(j);
            }
        }

        // Called When Place a node option is clicked from context menu
        function placeNode(coords) {
            if (xxdialogMode) return;
            var x = '<div style=margin-bottom:6px><label for=selectnode-search>' + "Buscar" + '</label>&nbsp&nbsp<input type=text placeholder="' + "Nombre del dispositivo" + '" id="selectnode-search" onchange=onPlaceNodeInputChange() onkeyup=onPlaceNodeInputChange() autocomplete=off style=width:120px></div><div id=placenode style="height:254px;overflow-y:auto;width:100%;margin:12px 1px 4px 1px;"><div id=noNodesMapPlace style=text-align:center;width:100%;display:none>' + "No se encontraron dispositivos." + '</div>';
            for (var i in nodes) {
                x += '<div class=noselect id=' + nodes[i]._id + '-rowid onclick=selectNodeToPlace(event,\'' + nodes[i]._id + '\') style=background-color:lightgray;margin-bottom:4px;border-radius:2px><input name=PlaceMapDeviceCheckbox id=' + nodes[i]._id + '-checkid type=checkbox class="form-check-input me-2" style=width:16px;display:inline />';
                x += '<div class=j' + nodes[i].icon + ' style=width:16px;height:16px;margin-top:2px;margin-right:4px;display:inline-block></div><div style=width:16px;display:inline>' + nodes[i].name + '</div></div>';
            }
            setModalContent('xxAddAgent', "Selecciona un nodo para colocar", x + '</div>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => placeNodeEx(3, coords));
            onPlaceNodeInputChange();
        }

        function placeNodeEx(button, coords) {
            var elements = document.getElementsByName('PlaceMapDeviceCheckbox');
            for (var i in elements) {
                if (elements[i].checked) {
                    var node = getNodeFromId(elements[i].id.substring(0, elements[i].id.length - 8));
                    if (node) {
                        var feature = xxmap.markersSource.getFeatureById(i);
                        var v = ol.proj.transform(coords, 'EPSG:3857', 'EPSG:4326');
                        var vx = [v[1], v[0]]; // Flip the coordinates around, lat/long
                        if (feature) {
                            feature.getGeometry().setCoordinates(coords);
                            var activeInteraction = getActiveInteractions(feature);
                            if (activeInteraction) {
                                saveMarkerloc(feature);
                            } else { // If this feature is not saved after its location is changed, then send updated coords to server.
                                meshserver.send({ action: 'changedevice', nodeid: node._id, userloc: vx }); // Send them to server to save changes
                            }
                        } else {
                            meshserver.send({ action: 'changedevice', nodeid: node._id, userloc: vx }); // This Node is not yet added to maps.
                        }
                    }
                }
            }
        }

        // Called when the user changes the search box
        function onPlaceNodeInputChange() {
            updatePlaceNodeTable(Q('selectnode-search').value.trim().toLowerCase());
        }

        // Update the list of devices in the "place on map" table
        function updatePlaceNodeTable(inputSearch) {
            var elements = document.getElementsByName('PlaceMapDeviceCheckbox'), count = 0;
            for (var i in nodes) {
                var visible = ((nodes[i].namel.indexOf(inputSearch) >= 0 || inputSearch == '') || (nodes[i].rnamel != null && nodes[i].rnamel.indexOf(inputSearch) >= 0));
                if (visible) { count++; }
                QV(nodes[i]._id + '-rowid', visible);
            }
            QV('noNodesMapPlace', count == 0);
            //console.log(selected);
            //for (var i in nodes) {
            //    if ((nodes[i].name.toLowerCase().indexOf(inputSearch) >= 0 || inputSearch == '') || (nodes[i].rnamel != null && nodes[i].rnamel.toLowerCase().indexOf(inputSearch) >= 0)) {
            //        console.log(selected.indexOf(nodes[i]._id));
            //        x += '<div class=noselect id=' + nodes[i]._id + '-rowid onclick=selectNodeToPlace(event,\''+ nodes[i]._id +'\') style=background-color:lightgray;margin-bottom:4px;border-radius:2px><input name=PlaceMapDeviceCheckbox id=' + nodes[i]._id + '-checkid type=checkbox style=width:16px;display:inline ' + ((selected.indexOf(nodes[i]._id) >= 0)?'checked':'') + ' />';
            //        x += '<div class=j' + nodes[i].icon + ' style=width:16px;height:16px;margin-top:2px;margin-right:4px;display:inline-block></div><div style=width:16px;display:inline>' + nodes[i].name + '</div></div>';
            //    }
            //}
            //if (x == '') { x = '<div style=text-align:center;width:100%>No devices found.</div>'; }
            //QH('placenode', '');
        }

        // Called when a user clicks on a device to toggle selection for placement on map.
        function selectNodeToPlace(e, id) {
            // Toggle checkbox if needed
            if (e.target.name != 'PlaceMapDeviceCheckbox') { var inputElement = Q(id + '-checkid'); inputElement.checked = !inputElement.checked; }

            // Check button state
            var elements = document.getElementsByName('PlaceMapDeviceCheckbox'), checkcount = 0;
            for (var i in elements) { if (elements[i].checked) checkcount++; }
            QE('idx_dlgOkButton', checkcount > 0);
        }

        // Add option for available meshes in mesh Dropdown
        function addMeshOptions(addMeshid, meshName) {
            //var meshOptions = Q('select-mesh');
            //if (addMeshid && meshName) {
            //    var option = document.createElement('option');
            //    option.value =addMeshid;
            //    option.text = meshName;
            //    meshOptions.add(option); // Add specific option
            //}
            //else {
            //    for (var i in meshes) { // Add all options
            //        var option = document.createElement('option');
            //        option.value = i;
            //        option.text = meshes[i].name;
            //        meshOptions.add(option);
            //    }
            //}
        }

        // Remove/Modify options in Mesh dropdown (if modMeshname is defined then Modify else Remove)
        function meshOptionRmvMod(delMeshid, modMeshname) {
            //var meshOptions = Q('select-mesh');
            //if (delMeshid) {
            //    var index=-1;
            //    for (var i = 1; i < meshOptions.options.length; i++) {
            //        if (meshOptions[i].value === delMeshid) { index=i; }
            //    }
            //    if (index > 0) {
            //        if (modMeshname) {
            //            meshOptions[index].innerHTML=modMeshname; // If Mesh name is Modified
            //        }
            //        else { meshOptions.remove(index); }
            //    }
            //}
        }

        //Check if there is any mesh created
        function meshExists() {
            for (var i in meshes) { if (meshes[i]) { return true; } }
            return false;
        }

        // Reset Mesh dropdown option to 'All' when a current view mesh is deleted.
        function setMeshView(emeshid) {
            var selectMeshElement = Q('select-mesh');
            var selectedIndex = selectMeshElement.selectedIndex;
            if (selectMeshElement[selectedIndex].value == emeshid) { selectMeshElement[0].selected = true; onSelectMeshChange(); }
        }

        // Clear all mesh options except 'All'
        function clearMeshOptions() {
            //var meshOptions=Q('select-mesh');
            //for(var i = meshOptions.options.length - 1 ; i > 0 ; i--) { meshOptions.remove(i); }
        }

        // Make a http get call
        function getSearchLocation() {
            try {
                var searchdata = Q('mapSearchLocation').value.trim();
                if (searchdata.length > 0) {
                    var xmlhttp = new XMLHttpRequest(); // Compatible with Chrome, Opera, Safari, IE7+, Firefox.
                    xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { formatSearchData(xmlhttp.responseText); } }
                    xmlhttp.open('GET', 'https://nominatim.openstreetmap.org/search?q=' + searchdata + '&format=json', true); // Get request
                    xmlhttp.send();
                }
            } catch (e) { }
        }

        // Format data recieved from nominatim API and display it on content window
        function formatSearchData(data) {
            try {
                QH('xmapSearchResults', '');
                var dataInfo = JSON.parse(data), count = 0, x = '<div class="xmapItem">';
                for (var i = 0; i < dataInfo.length; i++) {
                    if (dataInfo[i].display_name && dataInfo[i].boundingbox[0] && dataInfo[i].boundingbox[1] && dataInfo[i].boundingbox[2] && dataInfo[i].boundingbox[3]) {
                        count++;
                        var itemclass = (i % 2 == 0) ? 'xmapItemSel1' : 'xmapItemSel1';
                        x += '<div class="' + itemclass + '" onclick=mapGotoSelectedLocation(this)><div>' + dataInfo[i].display_name + '</div><div style=display:none>' + dataInfo[i].boundingbox[0] + '!#!' + dataInfo[i].boundingbox[1] + '!#!' + dataInfo[i].boundingbox[2] + '!#!' + dataInfo[i].boundingbox[3] + '</div></div>';
                    }
                }
                x += '</div>';
                if (count == 1) {
                    // If only one result is returned then zoom to that location
                    var extent = [parseFloat(dataInfo[0].boundingbox[2]), parseFloat(dataInfo[0].boundingbox[0]), parseFloat(dataInfo[0].boundingbox[3]), parseFloat(dataInfo[0].boundingbox[1])];
                    zoomToExtent(extent);
                } else {
                    if (count == 0) { x = '<div style=width:200px>' + "No se ha encontrado ninguna ubicación." + '<div>'; }
                    QV('xmapSearchResultsDlg', true);
                }
                QH('xmapSearchResults', x);
            }
            catch (e) { }
        }

        // Zoom into the bounding box
        function mapGotoSelectedLocation(obj) {
            var objchildren = obj.children;
            var boundingBox = objchildren[1].innerHTML.split('!#!');
            var extent = [parseFloat(boundingBox[2]), parseFloat(boundingBox[0]), parseFloat(boundingBox[3]), parseFloat(boundingBox[1])];
            //Q('search-location').value = objchildren[0].innerHTML;
            zoomToExtent(extent);
            mapCloseSearchWindow();
        }

        // Close the search window
        function mapCloseSearchWindow() {
            QH('xmapSearchResults', '');
            QV('xmapSearchResultsDlg', false);
        }

        // Zoom to specific cordinates
        function zoomToLocation(coordinates, zoomVal) {
            var view = xxmap.map.getView();
            view.setCenter(coordinates);
            view.setZoom(zoomVal);
        }

        function zoomToFitExtent() {
            var features = xxmap.markersSource.getFeatures();
            if (features.length > 0) {
                var extent = xxmap.markersSource.getExtent();
                xxmap.map.getView().fit(extent, xxmap.map.getSize());
            }
        }

        function zoomToExtent(extent) {
            var boundingExtent = ol.proj.transformExtent(extent, ol.proj.get('EPSG:4326'), ol.proj.get('EPSG:3857'));
            xxmap.map.getView().fit(boundingExtent, xxmap.map.getSize());
        }

        //
        // MY DEVICE
        //
        function refreshDevice(nodeid) {
            if (!currentNode || currentNode._id != nodeid) return;
            gotoDevice(nodeid, xxcurrentView, true);
        }

        var currentNode;
        var powerTimelineNode = null;
        var powerTimelineReq = null;
        var powerTimelineUpdate = null;
        var powerTimeline = null;

        var deviceSharesNode = null;
        var deviceSharesReq = null;
        var deviceShares = null;

        function getCurrentNode() { return currentNode; };
        function gotoDevice(nodeid, panel, refresh, event) {
            if (event && ((event.which == 3) || (event.button == 2))) return; // ignore right click events here as handled elsewhere
            // Remind the user to verify the email address
            if ((userinfo.emailVerified !== true) && (serverinfo.emailcheck == true) && (userinfo.siteadmin != 0xFFFFFFFF)) {
                setModalContent('xxAddAgent', "Seguridad de la Cuenta", "No se puede acceder a esta función hasta que se verifique una dirección de correo electrónico. Esto es necesario para recuperar la contraseña. Ve a la pestaña \"Mi cuenta\" para cambiar y verificar una dirección de correo electrónico.");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return;
            }

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0)) {
                setModalContent('xxAddAgent', "Seguridad de la Cuenta", "No se puede acceder a esta función hasta que se habilite la autenticación de dos factores. Esto es necesario para mayor seguridad. Ve a la pestaña \"Mi cuenta\" y revisa la sección \"Seguridad de la cuenta\".");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return;
            }

            if (event && ((event.shiftKey == true) || (event.which == 2) || (event.button == 1))) {
                // Open the device in a different tab
                safeNewWindow(window.location.origin + '?node=' + nodeid.split('/')[2] + '&viewmode=10&hide=16' + ((urlargs.key) ? ('&key=' + urlargs.key) : ''), 'meshcentral:' + nodeid);
                return;
            }

            //disconnectAllKvmFunction();
            var node = getNodeFromId(nodeid);
            if (node == null) return;
            if ((currentNode == null) || (currentNode._id != node._id) || ((node.conn & 1) == 0)) { deviceDetailsStatsClear(); } // Hide the list cpu/memory graph
            var mesh = meshes[node.meshid];
            var meshrights = GetNodeRights(node);
            var deviceSwitch = ((currentNode == null) || (currentNode._id != nodeid));
            if (!currentNode || currentNode._id != node._id || refresh == true) {
                currentNode = node;

                // Pre defined scripts
                QV('deskPreConfigScriptContextMenu1', (currentNode.mtype == 2) && isWindowsNode(currentNode)); // Windows devices
                QV('deskPreConfigScriptContextMenu2', (currentNode.mtype == 2) && !isWindowsNode(currentNode)); // Other devices

                // Device Notification
                QV('p10deviceNotify', (currentNode.sessions != null) && ((currentNode.sessions.kvm != null) || (currentNode.sessions.terminal != null) || (currentNode.sessions.files != null) || (currentNode.sessions.tcp != null) || (currentNode.sessions.udp != null)));
                QV('p10deviceStar', stars[currentNode._id] == 1);
                QV('p10deviceHelp', (currentNode.sessions != null) && (currentNode.sessions.help != null))
                if ((currentNode.sessions != null) && (currentNode.sessions.msg != null)) { QV('p10deviceMsg', true); QH('p10deviceMsg', Object.keys(currentNode.sessions.msg).length); } else { QV('p10deviceMsg', false); }

                // Device Battery
                QV('p10deviceBattery', false);
                if ((currentNode.sessions != null) && (currentNode.sessions.battery != null)) {
                    var bat = currentNode.sessions.battery;

                    var statestr = '';
                    if (bat.state == 'ac') { statestr = "El dispositivo está enchufado"; }
                    if (bat.state == 'dc') { statestr = "El dispositivo funciona con batería"; }

                    var levelstr = '', levelnum = -1;
                    if ((typeof bat.level == 'number') && (bat.level >= 0) && (bat.level <= 100)) {
                        levelstr = bat.level + '%';
                        levelnum = (Math.floor((bat.level + 10) / 25) + 1);
                        if (levelnum > 5) { lvl = 5; }
                        if (bat.state == 'ac') { if (bat.level == 100) { levelnum = 11; } else { levelnum += 5; } }
                    }

                    if (levelnum > 0) {
                        Q('p10deviceBattery').title = (statestr != null) ? (statestr + ', ' + levelstr) : levelstr;
                        QV('p10deviceBattery', true);
                        Q('p10deviceBattery').className = 'deviceBatteryLarge deviceBatteryLarge' + levelnum;
                    }
                } else {
                    QV('p10deviceBattery', false);
                }

                // Clear installedAppsData for Software tab
                installedAppsData = { desktop: [], store: [] };
                installedAppsLoading = false;
                installedAppsLoadingType = 0;
                showStoreApps = false;

                // Add node name
                var nname = '&nbsp;-&nbsp;' + EscapeHtml(node.name), nnameEx;
                if (nname.length == 0) { nname = ' - <i>' + "Ninguno" + '</i>'; }
                if (((meshrights & 4) != 0) && ((!mesh.flags) || ((mesh.flags & 2) == 0 || (mesh.flags & 16)))) { nname = '<span tabindex=0 title="' + "Haz clic aquí para editar el nombre del dispositivo del lado del servidor" + '" onclick=showEditNodeValueDialog(0) onkeyup="if (event.key == \'Enter\') showEditNodeValueDialog(0)" role="button">' + nname + ' <i class="fa-solid fa-pencil fa-2xs"/></i></span>'; }
                nnameEx = nname;
                if (mesh) { nname += '<span class="deviceGroupName" style=color:#AAA;font-size:small> - ' + EscapeHtml(mesh.name) + '</span>'; }
                QH('p10deviceName', nname);
                QH('p11deviceName', nname);
                QH('p12deviceName', nname);
                QH('p13deviceName', nname);
                QH('p14deviceName', nname);
                QH('p15deviceName', nname);
                QH('p16deviceName', nname);
                QH('p17deviceName', nname);
                QH('p18deviceName', nname);
                QH('p19deviceName', nname);

                // Node attributes
                var x = '<table style=width:100%>';

                // If title bar is hidden, display the device name here
                if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Nombre", nnameEx); }

                // Attribute: Mesh
                if (mesh) { x += addDeviceAttribute('<span title="' + "El nombre del grupo de dispositivos al que pertenece esta computadora." + '">' + "Grupo" + '</span>', '<a href=# title="' + "El nombre del grupo de dispositivos al que pertenece esta computadora" + '" onclick=gotoMesh("' + node.meshid + '") style=cursor:pointer>' + EscapeHtml(meshes[node.meshid].name) + '</a>'); }

                // Attribute: Name
                if ((node.rname != null) && (node.name != node.rname)) { x += addDeviceAttribute('<span title="' + "El nombre de esta computadora como se establece en el sistema operativo" + '">' + "Nombre del Sistema Operativo" + '</span>', '<span title="' + "El nombre de esta computadora como se establece en el sistema operativo" + '">' + EscapeHtml(node.rname) + '</span>'); }

                // Attribute: Host
                if ((((features & 1) == 0) && (node.mtype != 4)) || (node.mtype == 3)) { // If not WAN-only, local hostname is in use
                    x += addDeviceAttribute("Nombre de Host", addLinkConditional((node.host ? EscapeHtml(node.host) : ('<i>' + "Ninguno" + '</i>')), 'showEditNodeValueDialog(1)', meshrights & 4));
                }

                // Attribute: Description
                var description = node.desc ? EscapeHtml(node.desc) : ('<i>' + "Ninguno" + '</i>');
                if ((meshrights & 4) != 0) {
                    x += addDeviceAttribute("Descripción", '<span onclick=showEditNodeValueDialog(2) style=cursor:pointer>' + description + ' <i class="fa-solid fa-pencil fa-xs" role="button"></i></span>');
                } else {
                    x += addDeviceAttribute("Descripción", description);
                }

                // IP-KVM / PDU information
                if (node.mtype == 4) {
                    if (node.portnum != null) { x += addDeviceAttribute("Número de puerto", node.portnum); }
                    if (node.porttype != null) { x += addDeviceAttribute("Tipo de puerto", node.porttype); }
                }

                // Attribute: Mesh Agent
                if ((node.agent != null) && (node.agent.id != null) && (node.mtype == 3)) {
                    if (node.agent.id == 4) { x += addDeviceAttribute("Tipo de dispositivo", "Windows"); }
                    if (node.agent.id == 6) { x += addDeviceAttribute("Tipo de dispositivo", "Linux"); }
                    if (node.agent.id == 29) { x += addDeviceAttribute("Tipo de dispositivo", "macOS"); }
                } else if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
                    var str = '';
                    if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
                    if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
                    if (node.agent.id == 14) { str = node.agent.core; }
                    if ((node.agent.root === false) && ((node.conn & 1) != 0)) { str += ', ' + "Restringido"; }
                    x += addDeviceAttribute("Agente Mesh", str);
                }

                // Attribute: Intel AMT
                if (node.intelamt != null) {
                    var str = '';
                    var provisioningStates = { 0: nobreak("No Activada (Pre)"), 1: nobreak("No Activada (entrada)"), 2: nobreak("Activado") };
                    if (node.intelamt.ver != null && node.intelamt.state == null) { str += '<i>' + "Estado Desconocido" + '</i>, v' + EscapeHtml(node.intelamt.ver); }
                    else if ((node.intelamt.ver == null) && (node.intelamt.state == 2)) { str += '<i>' + "Activado" + '</i>'; }
                    else if ((node.intelamt.ver == null) || (node.intelamt.state == null)) { str += '<i>' + "Versión y Estado Desconocidos" + '</i>'; }
                    else {
                        str += provisioningStates[node.intelamt.state];
                        if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { str += ' <span title="' + "Intel&reg; AMT está activado en Modo de Control de Cliente (CCM)" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { str += ' <span title="' + "Intel&reg; AMT esta activado en Admin Control Mode (ACM)" + '">' + "ACM" + '</span>'; } }
                        str += (', v' + EscapeHtml(node.intelamt.ver));
                    }

                    // If Intel AMT is activated, show additional options
                    if (node.intelamt.state == 2) {
                        if (node.intelamt.tls == 1) { str += ', <span title="' + "Intel&reg; AMT está configurado con seguridad de red TLS" + '">' + "TLS" + '</span>'; }

                        var editUserCredentialsIcon = false;
                        if (node.intelamt.user == null || node.intelamt.user == '') { // If credentials are not set, allow setting them.
                            if ((meshrights & 4) != 0) {
                                str += ', <i style=color:#FF0000;cursor:pointer title="' + "Editar Credenciales de Intel&reg; AMT" + '" onclick=editDeviceAmtSettings("' + node._id + '")>' + "Sin Credenciales" + '</i>';
                                editUserCredentialsIcon = true;
                            } else {
                                str += ', <i style=color:#FF0000>' + "Sin Credenciales" + '</i>';
                            }
                        } else if (((features2 & 1) != 0) && (node.intelamt.warn != null)) { // If AMT manager is running and warned of invalid credentials, allow setting them.
                            var warn = null;
                            if ((node.intelamt.warn & 1) != 0) { warn = "Credenciales No Válidas"; }
                            if ((node.intelamt.warn & 8) != 0) { warn = "Probar Credenciales"; }
                            if (warn != null) {
                                if ((meshrights & 4) != 0) {
                                    str += ', <i style=color:#FF0000;cursor:pointer title="' + "Editar Credenciales de Intel&reg; AMT" + '" onclick=editDeviceAmtSettings("' + node._id + '")>' + warn + '</i>';
                                    editUserCredentialsIcon = true;
                                } else {
                                    str += ', <i style=color:#FF0000>' + warn + '</i>';
                                }
                            }
                        }

                        // If the AMT manager is not running, always allow Intel AMT credentials to be edited.
                        if (((meshrights & 4) != 0) && ((features2 & 1) == 0)) { editUserCredentialsIcon = true; }

                        str += ' ';
                        if (editUserCredentialsIcon) {
                            str += '<img src=images/link4.png height=10 width=10 title="' + "Editar Credenciales de Intel&reg; AMT" + '" style=cursor:pointer onclick=editDeviceAmtSettings("' + node._id + '")>';
                        }
                    }

                    var meName = '<span title="' + "Intel&reg; Manageability Engine" + '">' + "Intel&reg; ME" + '<span>';
                    if (typeof node.intelamt.sku == 'number') {
                        if ((node.intelamt.sku & 8) != 0) { meName = '<span title="' + "Intel&reg; Active Management Technology" + '">' + "Intel&reg; AMT" + '<span>'; }
                        else if ((node.intelamt.sku & 16) != 0) { meName = '<span title="' + "Intel&reg; Standard Manageability" + '">' + "Intel&reg; SM" + '<span>'; }
                    }
                    x += addDeviceAttribute(meName, str);
                }

                if ((node.agent != null) && (node.agent.tag != null)) {
                    // Attribute: Mesh Agent Tag
                    var tag = EscapeHtml(node.agent.tag);
                    if (tag.startsWith('mailto:')) { tag = '<a href="' + EscapeHtml(tag) + '">' + EscapeHtml(tag.substring(7)) + '</a>'; }
                    x += addDeviceAttribute("Etiqueta del Agente", tag);
                } else if ((node.intelamt != null) && (node.intelamt.tag != null)) {
                    // Attribute: Intel AMT Tag
                    var tag = EscapeHtml(node.intelamt.tag);
                    if (tag.startsWith('mailto:')) { tag = '<a href="' + EscapeHtml(tag) + '">' + EscapeHtml(tag.substring(7)) + '</a>'; }
                    x += addDeviceAttribute("Etiqueta de Intel&reg; AMT", tag);
                }

                // Attribute: Intel AMT
                //if (node.intelamt && node.intelamt.user) { x += addDeviceAttribute('Intel&reg; AMT', node.intelamt.user); }

                // Operating system description
                if (node.osdesc) { x += addDeviceAttribute("Sistema Operativo", EscapeHtml(node.osdesc)); }

                // Windows Security Central
                if (node.wsc) {
                    var y = [];
                    if (node.wsc.antiVirus != null) { if (node.wsc.antiVirus == 'OK') { y.push("AV" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("AV" + ' - <span style=color:red>' + "MALO" + '</span>'); } }
                    if (node.wsc.autoUpdate != null) { if (node.wsc.autoUpdate == 'OK') { y.push("Actualizar" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Actualizar" + ' - <span style=color:red>' + "MALO" + '</span>'); } }
                    if (node.wsc.firewall != null) { if (node.wsc.firewall == 'OK') { y.push("Cortafuegos" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Cortafuegos" + ' - <span style=color:red>' + "MALO" + '</span>'); } }
                    x += addDeviceAttribute("Seguridad de Windows", y.join(', '));
                }

                // Defender for Windows Server
                if (node.defender) {
                    var y = [];
                    if (node.defender.RealTimeProtection != null) { if (node.defender.RealTimeProtection == true) { y.push("RealTimeProtection" + ' - <span style=color:green>' + "On" + '</span>'); } else { y.push("RealTimeProtection" + ' - <span style=color:red>' + "Apagado" + '</span>'); } }
                    if (node.defender.TamperProtected != null) { if (node.defender.TamperProtected == true) { y.push("TamperProtection" + ' - <span style=color:green>' + "On" + '</span>'); } else { y.push("TamperProtection" + ' - <span style=color:red>' + "Apagado" + '</span>'); } }
                    if (node.defender.AntivirusSignatureVersion != null) { y.push("Versión de firma" + ' - <span style=color:green>' + EscapeHtml(node.defender.AntivirusSignatureVersion) + '</span>'); }
                    if (y.length > 0) x += addDeviceAttribute("Windows Defender", y.join(', '));
                }

                // Linux Security Central
                if (node.lsc) {
                    var y = [];
                    if (node.lsc.antiVirus != null) { if (node.lsc.antiVirus == 'OK') { y.push("AV" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("AV" + ' - <span style=color:red>' + "MALO" + '</span>'); } }
                    if (node.lsc.firewall != null) { if (node.lsc.firewall == 'OK') { y.push("Cortafuegos" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Cortafuegos" + ' - <span style=color:red>' + "MALO" + '</span>'); } }
                    if (y.length > 0) x += addDeviceAttribute("Seguridad de Linux", y.join(', '));
                }

                // Antivirus
                if (node.av && node.av.length > 0) {
                    var y = [];
                    for (var i in node.av) {
                        if (node.av[i].product) {
                            var avx = EscapeHtml(node.av[i].product);
                            if (node.av[i].enabled !== true) { avx += ' - <span style=color:red>' + "Deshabilitado" + '</span>'; }
                            if (node.av[i].updated !== true) { avx += ' - <span style=color:red>' + "Desactualizado" + '</span>'; }
                            if ((node.av[i].enabled == true) && (node.av[i].updated == true)) { avx += ' - <span style=color:green>' + "OK" + '</span>'; }
                            y.push(avx);
                        }
                    }
                    x += addDeviceAttribute("Antivirus", y.join('<br />'));
                }

                // Active Users
                if (node.users && node.users.length > 0) {
                    var u = node.users.map(function(user, index) {
                        var upn = node.upnusers && node.upnusers[index];
                        var label = (features3 & 0x00000002)
                            ? ((node.users && node.users[index] != null) ? EscapeHtml(node.users[index]) : null)
                            : (upn != null ? EscapeHtml(upn) : null);
                        var escapedUser = EscapeHtml((features3 & 0x00000002) && upn != null ? upn : user);
                        var locked = node.lusers && node.lusers.indexOf(user) >= 0;
                        if (locked) { return addKeyLinkConditional(escapedUser, label ? label + ' - ' + "Bloqueada" : "Bloqueada", true); }
                        if (label) { return '<span style=cursor:default title=\'' + label + '\'>' + escapedUser + '</span>'; }
                        return escapedUser;
                    }).join(', ');
                    x += addDeviceAttribute((node.users.length > 1 ? "Usuarios activos" : "Usuario activo"), u);
                }

                // Windows Idle Time
                if (node.idletime && node.idletime != -1) { x += addDeviceAttribute("Tiempo de inactividad", printTimer(node.idletime)); }

                // Display device user consent
                if ((node.agent != null) && (node.agent.id != 14) && (node.mtype != 3)) {
                    var meshFeatures = [];
                    var consent = 0;
                    if (node.consent) { consent = node.consent; }
                    if (serverinfo.consent) { consent |= serverinfo.consent; }
                    if ((consent & 0x0040) && (consent & 0x0008)) { meshFeatures.push("Mensaje de Escritorio + Barra de Herramientas"); } else if (consent & 0x0040) { meshFeatures.push("Barra de Herramientas de Escritorio"); } else if (consent & 0x0008) { meshFeatures.push("Mensaje de Escritorio"); } else { if (consent & 0x0001) { meshFeatures.push("Notificación de Escritorio"); } }
                    if (consent & 0x0010) { meshFeatures.push("Mensaje en Terminal"); } else { if (consent & 0x0002) { meshFeatures.push("Notificación de Terminal"); } }
                    if (consent & 0x0020) { meshFeatures.push("Solicitud de Archivos"); } else { if (consent & 0x0004) { meshFeatures.push("Notificación de Archivos"); } }
                    if (consent == 7) { meshFeatures = ["Siempre Notificar"]; }
                    if ((consent & 56) == 56) { meshFeatures = ["Siempre Preguntar"]; }

                    meshFeatures = meshFeatures.join(', ');
                    if (meshFeatures == '') { meshFeatures = '<i>' + "Ninguno" + '</i>'; }
                    x += addDeviceAttribute("Consentimiento del Usuario", addLinkConditional(meshFeatures, 'p20editmeshconsent(3)', meshrights & 1));
                }

                var devNotifyStr = [];
                if ((userinfo.notify != null) && (userinfo.notify[node._id] != null)) {
                    var devNotify = userinfo.notify[node._id];
                    if (devNotify & 2) { devNotifyStr.push("Conectar"); }
                    if (devNotify & 4) { devNotifyStr.push("Desconectar"); }
                    if ((node.intelamt != null) && (devNotify & 8)) { devNotifyStr.push("Intel&reg; AMT"); }
                    if ((features2 & 0x00004000) && (userinfo.emailVerified)) {
                        var xx = 0;
                        if (devNotify & 16) { xx++; } if (devNotify & 32) { xx++; } if (devNotify & 64) { xx++; }
                        if (xx > 0) { devNotifyStr.push(format("Correo Electronico ({0})", xx)); }
                    }
                    if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                        var xx = 0;
                        if (devNotify & 128) { xx++; } if (devNotify & 256) { xx++; } if (devNotify & 512) { xx++; }
                        if (xx > 0) { devNotifyStr.push(format("Mensajería ({0})", xx)); }
                    }
                }
                devNotifyStr = devNotifyStr.join(', ');
                if (devNotifyStr == '') { devNotifyStr = '<i>' + "Ninguno" + '</i>'; }
                x += addDeviceAttribute("Notificaciones", addLink(devNotifyStr, 'p20editDeviceNotify()'));

                // Attribute: Connectivity (Only show this if more than just the agent is connected).
                var connectivity = node.conn;
                if (connectivity && (connectivity > 1)) {
                    var cstate = [];
                    if ((node.conn & 1) != 0) cstate.push('<span title="' + "El Agente Mesh está conectado y listo para su uso." + '">' + "Agente Mesh" + '</span>');
                    if ((node.conn & 2) != 0) cstate.push('<span title="' + "Intel&reg; AMT CIRA está conectado y listo para su uso." + '">' + "Intel&reg; AMT CIRA" + '</span>');
                    else if ((node.conn & 4) != 0) cstate.push('<span title="' + "Intel&reg; AMT es enrutable y está listo para usar." + '">' + "Intel&reg; AMT" + '</span>');
                    if ((node.conn & 8) != 0) cstate.push('<span title="' + "Se puede alcanzar al Agente Mesh utilizando otro agente en modo Relay." + '">' + "Relé de Mesh" + '</span>');
                    if ((node.conn & 16) != 0) { cstate.push('<span title="' + "La conexión MQTT al dispositivo está activa." + '">' + "MQTT" + '</span>'); }
                    x += addDeviceAttribute("Conectividad", cstate.join(', '));
                }

                // Node tags
                var groupingTags = '<i>' + "Ninguno" + '</i>';
                if (node.tags != null) { groupingTags = ''; for (var i in node.tags) { groupingTags += '<span class=tagSpan>' + EscapeHtml(node.tags[i]) + '</span> '; } }
                if ((meshrights & 4) != 0) {
                    x += addDeviceAttribute("Etiquetas", '<span onclick=showEditNodeValueDialog(3) style=line-height:26px;cursor:pointer>' + groupingTags + ' <i class="fa-solid fa-pencil fa-xs" role="button"></i></span>');
                } else {
                    x += addDeviceAttribute("Etiquetas", '<span style=line-height:26px>' + groupingTags + '</span>');
                }

                // SSH & RDP Credentials
                if ((node.ssh != null) || (node.rdp != null)) {
                    var y = [];
                    if ((meshrights & 4) != 0) {
                        if (node.ssh != null) { y.push('<span onclick=showClearSshDialog(3) style=cursor:pointer>' + ((node.ssh == 1) ? "SSH-Usuario+Pass" : ((node.ssh == 2) ? "SSH-Usuario+Clave+Pasar" : "SSH-Usuario+Clave")) + ' <i class="fa-solid fa-pencil fa-xs" role="button"></i></span>'); }
                        if (node.rdp != null) { y.push('<span onclick=showClearRdpDialog(3) style=cursor:pointer>' + "RDP" + ' <i class="fa-solid fa-pencil fa-xs" role="button"></i></span>'); }
                    } else {
                        if (node.ssh != null) { y.push(((node.ssh == 1) ? "SSH-Usuario+Pass" : ((node.ssh == 2) ? "SSH-Usuario+Clave+Pasar" : "SSH-Usuario+Clave"))); }
                        if (node.rdp != null) { y.push("RDP"); }
                    }
                    x += addDeviceAttribute("Credenciales", y.join(', '));
                }

                // Relay for
                var relayFor = [];
                for (var i in meshes) { if (meshes[i].relayid == node._id) { relayFor.push('<a href=# onclick=gotoMesh("' + meshes[i]._id + '") style=cursor:pointer>' + EscapeHtml(meshes[i].name) + '</a>'); } }
                if (relayFor.length > 0) { x += addDeviceAttribute('<span title="' + "Grupos de dispositivos este dispositivo es un relé para" + '">' + "Relé para" + '</span>', relayFor.join(", ")); }

                x += '</table><br />';
                // Show action button, only show if we have permissions 4, 8, 64
                if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Acciones" + '" title="' + "Realizar acciones de energía en el dispositivo" + '" onclick=deviceActionFunction() />'; }
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Notas" + '" title="' + "Ver notas sobre este dispositivo" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Registrar Evento" + '" title="' + "Escribe un evento para este dispositivo" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
                if ((node.mtype == 2) && (connectivity & 1) && ((meshrights & 131072) != 0)) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" cmenu=deskPreConfigScriptContextMenu value="' + "Ejecutar" + '" title="' + "Ejecutar comandos en este dispositivo." + '" onclick=runDeviceCmd("' + encodeURIComponentEx(node._id) + '") />'; }
                if (node.mtype != 4) {
                    if ((meshrights & 8) && (connectivity & 1) && ((meshrights & 16384) != 0) || ((node.pmt == 1) && ((features2 & 2) != 0))) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Mensaje" + '" title="' + "Mostrar un mensaje de texto en el dispositivo remoto" + '" onclick=deviceMessageFunction() />'; }
                    //if ((connectivity & 1) && (meshrights & 8) && (node.agent.id < 5)) { x += '<input type=button value=Toast title="' + "Display a text message of the remote device" + '" onclick=deviceToastFunction() />';  }
                    if ((meshrights & 8) && (connectivity & 1) && ((meshrights & 16384) != 0) || ((node.pmt == 1) && ((features2 & 2) != 0))) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Chat" + '" title="' + "Abrir ventana de chat en esta computadora" + '" onclick=deviceChat(event) />'; }
                    if ((serverinfo != null) && (serverinfo.altmessenging != null) && (meshrights & 8) && (connectivity & 1)) {
                        for (var i in serverinfo.altmessenging) {
                            var am = serverinfo.altmessenging[i];
                            if ((am.type == null) || (am.type == 'device')) {
                                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + EscapeHtml(serverinfo.altmessenging[i].name) + '" onclick=altDeviceChat(event,' + i + ') />';
                            }
                        }
                    }
                    if ((serverinfo.guestdevicesharing !== false) && (node.agent != null) && (node.agent.caps & 3) && (connectivity & 1) && ((meshrights & 0x80008) == 0x80008) && ((meshrights == 0xFFFFFFFF) || ((meshrights & 0x1000) == 0))) {
                        x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Compartir" + '" title="' + "Crear un enlace para compartir este dispositivo con un invitado" + '" onclick=showShareDevice() />';
                        QV('DeskGuestShareButton', true);
                    } else {
                        QV('DeskGuestShareButton', false);
                    }
                }
                if ((node.mtype == 4) && (connectivity & 1)) {
                    if (node.porttype == 'PDU') {
                        if (node.pwr == 1) {
                            if (meshrights & 0x40000) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Apagar" + '" title="' + "Apagar" + '" onclick=setIpPduState(0) />'; }
                        } else if (node.pwr == 8) {
                            if (meshrights & 0x40) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Encender" + '" title="' + "Encender" + '" onclick=setIpPduState(1) />'; }
                        }
                    } else {
                        if (meshrights & 8) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Control Remoto" + '" title="' + "Control Remoto" + '" onclick=openIpKvmRemoteControl("' + encodeURIComponentEx(node._id) + '") />'; }
                    }
                }

                // Custom UI
                if ((customui != null) && (customui.devicebuttons != null)) {
                    for (var i in customui.devicebuttons) {
                        if ((customui.devicebuttons[i].showif == null) || ((mesh != null) && (customui.devicebuttons[i].showif == mesh._id)) || (customui.devicebuttons[i].showif == currentNode._id)) {
                            x += '<input id="cui:' + i + '" type="button" class="btn btn-primary btn-sm me-2 mb-2" value="' + customui.devicebuttons[i].name + '" onclick=customUIAction(event,"devicebuttons") />&nbsp;';
                        }
                    }
                }

                QH('p10html', x);

                // Show node last 7 days timeline
                mainUpdate(256);

                // Check if we have terminal and file access
                var desktopAccess = ((meshrights == 0xFFFFFFFF) || ((meshrights & 65536) == 0));
                var terminalAccess = ((meshrights == 0xFFFFFFFF) || ((meshrights & 512) == 0));
                var fileAccess = ((meshrights == 0xFFFFFFFF) || ((meshrights & 1024) == 0));
                var amtAccess = ((meshrights == 0xFFFFFFFF) || ((meshrights & 2048) == 0));

                // Show bottom buttons
                x = '<div class="p10html3right">';
                if (((meshrights & 1) != 0) && (node.mtype != 4)) { // MESHRIGHT_EDITMESH
                    // TODO: Show change group only if there is another mesh of the same type.
                    x += '&nbsp;<a href=# onclick=p10showChangeGroupDialog(["' + node._id + '"]) title="' + "Mover este dispositivo a un grupo de dispositivos diferente" + '">' + "Cambiar Grupo de Dispositivos" + '</a>';
                }
                if ((meshrights & 0x8000) != 0) { // MESHRIGHT_UNINSTALL
                    x += '&nbsp;<a href=# onclick=p10showDeleteNodeDialog("' + node._id + '") title="' + "Remover este dispositivo" + '">' + "Eliminar Dispositivo" + '</a>';
                }
                x += '</div><div class="p10html3left">';
                if ((node.agent) && (node.mtype != 3) && ((meshrights & 1048576) != 0)) x += '<a href=# onclick=p10showNodeNetInfoDialog("' + node._id + '") title="' + "Mostrar información de la interfaz de red del dispositivo" + '">' + "Interfaces" + '</a>&nbsp;';
                if ((features & 0x00008000) && (xxmap != null)) x += '<a href=# onclick=p10showNodeLocationDialog("' + node._id + '") title="' + "Mostrar información de ubicaciones del dispositivo" + '">' + "Ubicación" + '</a>&nbsp;';
                if ((node.agent == null) || ((node.agent.id != 14) && (node.agent.id != 34))) {
                    if ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 128) == 0)) { // Check if we should view tools
                        if ((terminalAccess) && ((meshrights & 8) != 0) && (node.agent != null) && (node.agent.id != 14)) x += '<a href=# onclick=p10showMeshCmdDialog(1,"' + node._id + '") title="' + "Enrutador de tráfico utilizado para conectarse a un dispositivo a través de este servidor" + '.">' + "MeshCmd" + '</a>&nbsp;';
                    }
                    if ((args.xterm === 0) && (node.agent) && ((node.agent.caps & 2) != 0) && ((meshrights & 8) != 0) && ((meshrights == 0xFFFFFFFF) || ((meshrights & 512) == 0))) { x += '<a href=# onclick=p10openxterm(event,"' + node._id + '") title="' + "Abrir Terminal XTerm" + '">' + "XTerm" + '</a>&nbsp;'; }

                    // RDP link, show this link only of the remote machine is Windows.
                    if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) {
                        if (webRelayPort != 0) {
                            x += '<a href=# cmenu=httpPortContextMenu onclick=p10WebRouter("' + node._id + '",1,' + (node.httpport ? node.httpport : 80) + ')>' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + '</a>&nbsp;';
                            x += '<a href=# cmenu=httpsPortContextMenu onclick=p10WebRouter("' + node._id + '",2,' + (node.httpsport ? node.httpsport : 443) + ')>' + "HTTPS" + ((node.httpsport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + '</a>&nbsp;';
                        }
                        if ((node.agent.id > 0) && (node.agent.id < 5)) {
                            if (navigator.platform.toLowerCase() == 'win32') {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
                                    x += '<a href=# cmenu=altPortContextMenu id=rdpMCRouterLink onclick=p10MCRouter("' + node._id + '",3) title="' + "Requiere la instalación de MeshCentral Router" + '.">' + "RDP" + ((node.rdpport && (node.rdpport != 3389)) ? '/' + node.rdpport : '') + '</a>&nbsp;';
                                }
                            }
                        }
                        if (node.agent.id > 4) {
                            if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
                                    x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ') title="' + "Requiere la instalación de MeshCentral Router." + '">' + "SSH" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
                                }
                            }
                            if (navigator.platform.toLowerCase() == 'win32') {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
                                    x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ') title="' + "Requiere la instalación de MeshCentral Router." + '">' + "SCP" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
                                }
                            }
                        }
                        if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                            if ((serverinfo.devicemeshrouterlinks != null) && (Array.isArray(serverinfo.devicemeshrouterlinks.extralinks))) {
                                for (var i in serverinfo.devicemeshrouterlinks.extralinks) {
                                    var r = serverinfo.devicemeshrouterlinks.extralinks[i], p = '\"' + r.protocol + '\"';;
                                    if (doesDeviceMatchFilterTags(node, r.filter)) {
                                        if (typeof r.protocol == 'number') { p = r.protocol; } else if (r.protocol == 'http') { p = 1; } else if (r.protocol == 'https') { p = 2; } else if (r.protocol == 'rdp') { p = 3; } else if (r.protocol == 'ssh') { p = 4; } else if (r.protocol == 'scp') { p = 5; } else if (r.protocol == 'mcrdesktop') { p = 6; } else if (r.protocol == 'mcrfiles') { p = 7; }
                                        if (((p == 6) || (p == 7)) && (node.mtype != 2)) continue; // If this is not an agent device, don't show MeshCentral Router desktop/file links.
                                        x += '<a href=# onclick=p10MCRouter("' + node._id + '",' + p + ',' + r.port + (r.ip ? (',\"' + r.ip + '\"') : ',null') + ',' + (r.localport ? r.localport : 0) + ') title="' + "Requiere la instalación de MeshCentral Router." + '">' + r.name + '</a>&nbsp;';
                                    }
                                }
                            }
                        }
                    }

                    // noVNC link
                    if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && ((features & 0x20000000) == 0)) {
                        x += '<a href=# cmenu=rfbPortContextMenu id=rfbLink onclick=p10rfb("' + node._id + '") title="' + "Inicia una sesión VNC basada en web en este dispositivo" + '.">' + "Web-VNC" + '</a>&nbsp;';
                    }

                    // MSTSC.js link
                    if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && ((features & 0x40000000) == 0)) {
                        x += '<a href=# cmenu=altPortContextMenu id=mstscLink onclick=p10mstsc("' + node._id + '") title="' + "Ejecutar sesión de RDP basada en el navegador para este dispositivo" + '.">' + "Web-RDP" + '</a>&nbsp;';
                    }

                    // SSH link
                    if ((features2 & 0x200) && (((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) {
                        x += '<a href=# cmenu=sshPortContextMenu id=sshLink onclick=p10ssh("' + node._id + '") title="' + "Inicia una sesión SSH basada en web en este dispositivo" + '.">' + "Web-SSH" + '</a>&nbsp;';
                    }

                    // MQTT options
                    if ((meshrights == 0xFFFFFFFF) && (features & 0x00400000)) { x += '<a href=# onclick=p10showMqttLoginDialog("' + node._id + '") title="' + "Obtener las credenciales de inicio de sesión MQTT para este dispositivo." + '">' + "Inicio de Sesión MQTT" + '</a>&nbsp;'; }
                }
                x += '</div><br>'

                if (node.mtype == 3) {
                    // If this is a local device, there is no power timeline so display the links below the user rights.
                    QH('p10html3', '');
                    QH('p10html5', x);
                } else {
                    // This is a normal device, display the links below the power timeline.
                    QH('p10html3', x);
                    QH('p10html5', '');
                }

                // Set the node power state
                var powerstate = PowerStateStr(node.state);
                //if (node.state == 0) { powerstate = 'Unknown State'; }
                var agentPrivilages = '';
                if ((node.agent != null) && (node.agent.root === false)) { agentPrivilages = ', <span title="' + "El agente se ejecuta en un dispositivo remoto con privilegios reducidos." + '">' + "Restringido" + '</span>'; }
                if ((connectivity & 1) != 0) {
                    if (powerstate.length > 0) { powerstate += '<br/>'; }
                    if (node.mtype == 4) {
                        if (node.porttype == 'PDU') {
                            powerstate += '<span style=font-size:12px title="' + "Puerto de conmutador conectado" + '">' + "Puerto de conmutador conectado" + '</span>' + agentPrivilages;
                        } else {
                            powerstate += '<span style=font-size:12px title="' + "Puerto IP-KVM conectado" + '">' + "Puerto IP-KVM conectado" + '</span>' + agentPrivilages;
                        }
                    } else {
                        powerstate += '<span style=font-size:12px title="' + "Agente conectado" + '">' + "Agente conectado" + '</span>' + agentPrivilages;
                    }
                }
                if ((connectivity & 2) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT conectado" + '">' + "Intel&reg; AMT conectado" + '</span>'; }
                else if ((connectivity & 4) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT detectado" + '">' + "Intel&reg; AMT detectado" + '</span>'; }
                if ((connectivity & 16) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "MQTT conectado" + '">' + "Canal MQTT conectado" + '</span>'; }
                if ((powerstate == '') && node.lastconnect) { powerstate = '<span style=font-size:12px>' + "Visto por última vez:" + '<br />' + printDateTime(new Date(node.lastconnect)) + '</span>'; }
                else {
                    if (node.porttype == 'PDU') { powerstate += ('<br/>' + powerStateStrings[node.pwr]); }
                    else if ((node.pwr > 1) && (node.pwr != 7)) { powerstate += ('<br/>' + powerStateStrings[node.pwr]); }
                }
                QH('MainComputerState', powerstate);

                // Set the node icon
                Q('MainComputerImage').setAttribute('src', 'images/icons256-' + node.icon + '-1.png');
                Q('MainComputerImage').className = ((((!node.conn) || (node.conn == 0)) && (node.mtype != 3)) ? 'gray' : '');

                // If we are looking at a local non-windows device, enable terminal capability.
                if ((node.mtype == 3) && (node.agent != null) && (node.agent.id > 4) && (features2 & 0x00000200)) { node.agent.caps = 6; } // 1 = Terminal, 2 = Desktop, 4 = files

                // Setup/Refresh the desktop tab
                if (terminalAccess) { setupTerminal(); }
                if (fileAccess) { setupFiles(); }
                var consoleRights = ((meshrights & 16) != 0);
                if (consoleRights) { setupConsole(); } else { if (panel == 15) { panel = 10; } }

                // Show or hide the tabs
                // mesh.mtype: 1 = Intel AMT only, 2 = Mesh Agent, 3 = Local Device
                // node.agent.caps (bitmask): 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console
                QV('MainDevDesktop', desktopAccess && ((((node.agent == null) && (node.intelamt != null) && ((typeof node.intelamt.sku !== 'number') || ((node.intelamt.sku & 8) != 0)))
                    || ((node.agent != null) && ((node.agent.caps == null) || ((node.agent.caps & 1) != 0) || (node.intelamt && (node.intelamt.state == 2)))) || ((node.mtype == 3) && ((node.agent.id == 3) || (node.agent.id == 4))))
                    && ((meshrights & 8) || (meshrights & 256)))
                );
                QV('MainDevTerminal', (((node.agent == null) && (node.intelamt != null)) || ((node.agent) && (node.agent.caps == null)) || ((node.agent) && ((node.agent.caps & 2) != 0)) || (node.intelamt && (node.intelamt.state == 2))) && (meshrights & 8) && terminalAccess);
                QV('MainDevFiles', (node.agent != null) && (node.agent.caps != null) && ((node.agent.caps & 4) != 0) && (meshrights & 8) && fileAccess);
                QV('MainDevInfo', (node.mtype < 3) && ((meshrights & 1048576) != 0));
                QV('MainDevApps', (node.mtype == 2) && (serverinfo.softwareinventory === true && (meshrights & 1048576) != 0));
                QV('MainDevAmt', (node.intelamt != null) && ((node.intelamt.state == 2) || (node.conn & 2)) && (meshrights & 8) && amtAccess);
                QV('MainDevConsole', (consoleRights && ((node.agent != null) && (node.agent.caps != null) && ((node.agent.caps & 8) != 0))) && (meshrights & 8));
                QV('MainDevPlugins', false);
                QV('p15uploadCore', (node.agent != null) && (node.agent.caps != null) && ((node.agent.caps & 16) != 0));
                QH('p15coreName', ((node.agent != null) && (node.agent.core != null)) ? EscapeHtml(node.agent.core) : '');

                // Set the Intel AMT / Intel SM tab name
                if ((node.intelamt != null) && (typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
                    QH('MainDevAmt', "Intel&reg;SM");
                    QH('p14deviceNamePrefix', "Intel&reg; SM");
                } else {
                    QH('MainDevAmt', "Intel&reg;AMT");
                    QH('p14deviceNamePrefix', "Intel&reg; AMT");
                }

                // Setup/Refresh Intel AMT tab
                var amtFrameNode = Q('p14iframe').contentWindow.getCurrentMeshNode();
                if ((amtFrameNode != null) && (amtFrameNode._id != currentNode._id)) { Q('p14iframe').contentWindow.disconnect(); }
                var online = ((node.conn & 6) != 0); // If CIRA (2) or AMT (4) connected, enable Commander
                Q('p14iframe').contentWindow.setConnectionState(online);
                Q('p14iframe').contentWindow.setFrameHeight('650px');
                Q('p14iframe').contentWindow.setAuthCallback(updateAmtCredentials);

                // Request the power timeline
                if ((powerTimelineNode != currentNode._id) && (powerTimelineReq != currentNode._id)) {
                    QH('p10html2', '');
                    powerTimelineReq = currentNode._id;
                    meshserver.send({ action: 'powertimeline', nodeid: currentNode._id });
                    meshserver.send({ action: 'lastconnect', nodeid: currentNode._id });
                    meshserver.send({ action: 'getsysinfo', nodeid: currentNode._id });
                    meshserver.send({ action: 'getnetworkinfo', nodeid: currentNode._id });
                    meshserver.send({ action: 'getNotes', id: currentNode._id });
                    QH('p17info2', '');
                }

                // Request device sharing
                if ((deviceSharesNode != currentNode._id) && (deviceSharesReq != currentNode._id)) {
                    deviceSharesReq = currentNode._id;
                    meshserver.send({ action: 'deviceShares', nodeid: currentNode._id });
                }

                // Reset the desktop tools
                QV('DeskTools', false);
                showDeskToolsProcesses();

                // Ask for device events
                refreshDeviceEvents();

                // Update the web page title
                if ((currentNode) && (xxcurrentView >= 10) && (xxcurrentView < 20)) {
                    document.title = currentNode.name + (mesh ? (' - ' + mesh.name) : '') + ' - ' + decodeURIComponent('{{{extitle}}}');
                } else {
                    document.title = decodeURIComponent('{{{extitle}}}');
                }

                // Clear user consent status if present
                if (deviceSwitch) {
                    p11clearConsoleMsg();
                    p12clearConsoleMsg();
                    p13clearConsoleMsg();
                }

                // Device refresh plugin handler
                if (pluginHandler != null) {
                    QH('p19headers', ''); QH('p19pages', '');
                    pluginHandler.callHook('onDeviceRefreshEnd', nodeid, panel, refresh, event);
                    var lastTab = getstore('_curPluginPage', null);
                    if (lastTab != null && Q('p19ph-' + lastTab) != null) pluginHandler.callPluginPage(lastTab, Q('p19ph-' + lastTab));
                }

                // Show user device permissions
                x = '';
                if (meshrights & 7) {
                    x += '<button class="btn btn-primary btn-sm me-2 mb-2" onclick="return p20showAddMeshUserDialog(5)" style=cursor:pointer;margin-right:10px><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Usuario" + '</button>';
                    if (usergroups != null) {
                        var userGroupCount = 0, newUserGroup = false;
                        for (var i in usergroups) {
                            if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != nodeid.split('/')[1])) continue;
                            userGroupCount++; if ((currentNode.links == null) || (currentNode.links[i] == null)) { newUserGroup = true; }
                        }
                        if ((userGroupCount > 0) && (newUserGroup)) { x += '<a href=# onclick="return p20showAddMeshUserDialog(6)" style=cursor:pointer;margin-right:10px><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Grupo de Usuarios" + '</a>'; }
                    }
                }

                x += '<table class="table table-hover" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col style=text-align:left;width:430px>' + "Autorizaciones de Usuario" + '</th><th scope=col style=text-align:left></th></tr>';
                var count = 1;
                if (currentNode.links != null) {
                    // Sort the list of users to display
                    var useridlist = [];
                    for (var i in currentNode.links) { if (i.startsWith('user/') || i.startsWith('ugrp/')) { useridlist.push(i); } }
                    useridlist.sort();
                    for (var i in useridlist) {
                        var trash = '', rights = '', userid = useridlist[i], srights = currentNode.links[userid].rights, username = EscapeHtml(userid.split('/')[2]), rights = makeUserDeviceRightsString(srights), ugroup = false;
                        if (currentNode.links[userid].name != null) { username = EscapeHtml(currentNode.links[userid].name); }
                        if (userid == userinfo._id) { username = EscapeHtml(userinfo.name); }
                        if ((users != null) && (users[userid] != null)) { username = EscapeHtml(users[userid].name); }
                        if ((usergroups != null) && (usergroups[userid] != null)) { username = EscapeHtml(usergroups[userid].name); ugroup = true; }
                        if ((meshrights & 2) != 0) {
                            if (ugroup) {
                                trash = '<a href=# onclick=\'return p30removeUserFromNode(event,"' + encodeURIComponentEx(userid) + '")\' title="' + "Remover los permisos del grupo de usuarios a este grupo de dispositivos" + '" style=cursor:pointer><i class="fa-solid fa-trash text-danger hoverButton"></i></a>';
                                rights = '<span style=cursor:pointer onclick=p20showAddMeshUserDialog(6,"' + encodeURIComponentEx(userid) + '")>' + rights + ' <img class=hoverButton style=cursor:pointer src=images/link5.png></span>';
                            } else {
                                trash = '<a href=# onclick=\'return p30removeUserFromNode(event,"' + encodeURIComponentEx(userid) + '")\' title="' + "Remover permisos de usuario de este grupo de dispositivos" + '" style=cursor:pointer><i class="fa-solid fa-trash text-danger hoverButton"></i></a>';
                                rights = '<span style=cursor:pointer onclick=p20showAddMeshUserDialog(5,"' + encodeURIComponentEx(userid) + '")>' + rights + ' <img class=hoverButton style=cursor:pointer src=images/link5.png></span>';
                            }
                        }
                        if (users != null) {
                            if (ugroup) {
                                username = '<a href=# onclick=\'gotoUserGroup("' + encodeURIComponentEx(userid) + '");haltEvent(event);\'>' + username + '</a>';
                            } else {
                                username = '<a href=# onclick=\'gotoUser("' + encodeURIComponentEx(userid) + '");haltEvent(event);\'>' + username + '</a>';
                            }
                        }

                        x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%><div title="' + (ugroup ? "Grupo de Usuarios" : "Usuario") + '" class=m' + (ugroup ? 4 : 2) + '></div><div>&nbsp;' + username + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
                    }
                }
                if (count == 1) { x += '<tr><td><div style=padding:6px>&nbsp;<i>' + "No hay usuarios con permisos especiales para el dispositivo" + '</i><div></div></div></td><td></td></tr>'; }
                x += '</tbody></table>';

                // Show device shares
                if ((deviceShares != null) && (deviceSharesNode == currentNode._id) && (deviceShares.length > 0)) {
                    x += '<br /><table class="table table-hover" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col style=text-align:left;width:430px>' + "Compartir dispositivo activo" + '</th><th scope=col style=text-align:left></th></tr>';
                    count = 1;
                    for (var i = 0; i < deviceShares.length; i++) {
                        var dshare = deviceShares[i], trash = '';
                        if (dshare.url != null) { trash += '<a href="' + dshare.url + '" rel="noreferrer noopener" target=_blank title="' + "Enlace para compartir dispositivo" + '" style=cursor:pointer><i class="fa-fw fa-solid fa-share-from-square"></i></a> '; }
                        trash += '<i role=button onclick=\'return p30removeDeviceSharing(event,"' + encodeURIComponentEx(currentNode._id) + '","' + encodeURIComponentEx(dshare.publicid) + '","' + encodeURIComponentEx(dshare.guestName) + '")\' title="' + "Eliminar dispositivo compartido" + '" class="fa-fw fa-solid fa-trash text-danger"></i>';
                        var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Escritorio", "Escritorio + Terminal", "Archivos", "Terminal + Archivos", "Escritorio + Archivos", "Escritorio + Terminal + Archivos"][dshare.p]; } else if (dshare.p == 8) { type = "HTTP/" + dshare.port; } else if (dshare.p == 16) { type = "HTTPS/" + dshare.port; }
                        var details = type;
                        if ((dshare.startTime != null) && (dshare.expireTime != null)) { details = format("{0}, {1} a {2}", type, printFlexDateTime(new Date(dshare.startTime)), printFlexDateTime(new Date(dshare.expireTime))); }
                        if ((dshare.startTime != null) && (dshare.duration != null)) {
                            if (dshare.duration < 2) {
                                details = format("{0}, {1} por {2} minuto", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            } else {
                                details = format("{0}, {1} durante {2} minutos", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            }
                        }
                        if (dshare.recurring == 1) { details += ", Recurrente diariamente"; }
                        if (dshare.recurring == 2) { details += ", Recurrente semanalmente"; }
                        if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Ver solo escritorio"; }
                        if (dshare.consent != null) {
                            if (dshare.consent == 0) { details += ", Sin consentimiento"; } else {
                                if ((dshare.consent & 0x0038) != 0) { details += ", Solicitud de consentimiento"; }
                                if ((dshare.consent & 0x0040) != 0) { details += ", Barra de herramientas"; }
                            }
                        }
                        var guestName = EscapeHtml(dshare.guestName);
                        if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Autocompartido del agente" + '</i>'; }
                        x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%><div class=m' + 2 + '></div><div>&nbsp;' + guestName + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + details + '</div></td></tr>';
                    }
                    x += '</tbody></table>';
                }
                QH('p10html4', x);

                // Change the URL
                var urlviewmode = '';
                if (((features & 0x10000000) == 0) && (xxcurrentView >= 10) && (xxcurrentView <= 19) && (currentNode != null)) {
                    urlviewmode = '?viewmode=' + xxcurrentView + '&gotonode=' + currentNode._id.split('/')[2];
                    for (var i in urlargs) { urlviewmode += ('&' + i + '=' + urlargs[i]); }
                    try { window.history.replaceState({}, document.title, window.location.pathname + urlviewmode); } catch (ex) { }
                }

                // Clear the desktop session selector
                QV('p11DeskSessionSelector', false);
                QH('p11DeskSessionSelector', '');
            }
            setupDesktop(); // Always refresh the desktop, even if we are on the same device, we need to do some canvas switching.
            if (!panel) panel = 10;
            go(panel);
        }

        function setIpPduState(op) {
            if (op == 0) {
                // Turn off
                setModalContent('xxAddAgent', "Operación de energía", 'Perform power off?');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: 2 }); });
            } else {
                // Turn on
                setModalContent('xxAddAgent', "Operación de energía", 'Perform power on?');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'wakedevices', nodeids: [currentNode._id] }); });
            }
        }

        function p20editDeviceNotify() {
            if (xxdialogMode) return false;
            var devNotify = 0, fx = ((features2 & 0x00004000) && (userinfo.emailVerified)) ? 1 : 0;
            if (userinfo.notify && userinfo.notify[currentNode._id]) { devNotify = userinfo.notify[currentNode._id]; }
            var x = '<div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificaciones de página web" + '</div>';
            x += '<div><label><input id=p20notifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Conexiones de dispositivo" + '</label></div>';
            x += '<div><label><input id=p20notifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Desconexiones de dispositivo" + '</label></div>';
            if (currentNode.intelamt != null) { fx += 2; x += '<div><label><input id=p20notifyIntelAmtKvmActions type=checkbox class="form-check-input me-2" />' + "Eventos de escritorio y serial de Intel&reg; AMT" + '</label></div>'; }
            if (fx & 1) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificaciones por correo electrónico" + '</div>';
                x += '<div><label><input id=p20enotifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Conexiones de dispositivo" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Desconexiones de dispositivo" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceHelp type=checkbox class="form-check-input me-2" />' + "Solicitudes de ayuda" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificaciones de Mensajería" + '</div>';
                x += '<div><label><input id=p20emsgDeviceConnect type=checkbox class="form-check-input me-2" />' + "Conexiones de dispositivo" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Desconexiones de dispositivo" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceHelp type=checkbox class="form-check-input me-2" />' + "Solicitudes de ayuda" + '</label></div>';
            }
            setModalContent('xxAddAgent', "Opciones de Notificación", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p20editDeviceNotifyEx(3, fx));
            Q('p20notifyIntelDeviceConnect').checked = (devNotify & 2);
            Q('p20notifyIntelDeviceDisconnect').checked = (devNotify & 4);
            if (fx & 2) { Q('p20notifyIntelAmtKvmActions').checked = (devNotify & 8); }
            if (fx & 1) {
                Q('p20enotifyIntelDeviceConnect').checked = (devNotify & 16);
                Q('p20enotifyIntelDeviceDisconnect').checked = (devNotify & 32);
                Q('p20enotifyIntelDeviceHelp').checked = (devNotify & 64);
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                Q('p20emsgDeviceConnect').checked = (devNotify & 128);
                Q('p20emsgDeviceDisconnect').checked = (devNotify & 256);
                Q('p20emsgDeviceHelp').checked = (devNotify & 512);
            }
            return false;
        }

        function p20editDeviceNotifyEx(b, fx) {
            var devNotify = 0;
            devNotify += Q('p20notifyIntelDeviceConnect').checked ? 2 : 0;
            devNotify += Q('p20notifyIntelDeviceDisconnect').checked ? 4 : 0;
            if (fx & 2) { devNotify += Q('p20notifyIntelAmtKvmActions').checked ? 8 : 0; }
            if (fx & 1) {
                devNotify += Q('p20enotifyIntelDeviceConnect').checked ? 16 : 0;
                devNotify += Q('p20enotifyIntelDeviceDisconnect').checked ? 32 : 0;
                devNotify += Q('p20enotifyIntelDeviceHelp').checked ? 64 : 0;
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                devNotify += Q('p20emsgDeviceConnect').checked ? 128 : 0;
                devNotify += Q('p20emsgDeviceDisconnect').checked ? 256 : 0;
                devNotify += Q('p20emsgDeviceHelp').checked ? 512 : 0;
            }
            meshserver.send({ action: 'changeusernotify', nodeid: currentNode._id, notify: devNotify });
        }

        function makeUserDeviceRightsString(rights) {
            if (rights == 57592) { return "Todos los Permisos en el Dispositivo"; }
            var str = [];
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Sin Entrada");
                if (rights & 512) str1.push("Sin Terminal");
                if (rights & 1024) str1.push("Sin Archivos");
                if (rights & 2048) str1.push("Sin AMT");
                if (rights & 4096) str1.push("Entrada Limitada");
                if (rights & 65536) str1.push("Sin Escritorio");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Invitado Compartir");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Control"); }
            }
            if (rights & 16) str.push("Consola");
            if (rights & 32) str.push("Archivos del Servidor");
            if (rights & 64) str.push("Despertar");
            if (rights & 128) str.push("Notas");
            if (rights & 8192) str.push("Limitar Eventos");
            if (rights & 16384) str.push("Chat");
            if (rights & 32768) str.push("Desinstalar");
            if (rights & 131072) str.push("Comandos");
            if (rights & 262144) str.push("Reset/Apagado");
            if (rights & 524288) str.push("Intercambio");
            if (rights & 1048576) str.push("Detalles");
            if (rights & 2097152) str.push("Relé");
            if (str.length == 0) return "Sin Permisos";
            return str.join(', ');
        }

        function makeDeviceGroupRightsString(rights) {
            if (rights == 0xFFFFFFFF) { return "Todos los Permisos"; }
            var str = [];
            if (rights & 1) str.push("Editar Grupo");
            if (rights & 2) str.push("Administrar Usuarios");
            if (rights & 4) str.push("Administrar Dispositivos");
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Sin Entrada");
                if (rights & 512) str1.push("Sin Terminal");
                if (rights & 1024) str1.push("Sin Archivos");
                if (rights & 2048) str1.push("Sin AMT");
                if (rights & 4096) str1.push("Entrada Limitada");
                if (rights & 65536) str1.push("Sin Escritorio");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Invitado Compartir");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Control"); }
            }
            if (rights & 16) str.push("Consola");
            if (rights & 32) str.push("Archivos del Servidor");
            if (rights & 64) str.push("Despertar");
            if (rights & 128) str.push("Notas");
            if (rights & 8192) str.push("Limitar Eventos");
            if (rights & 16384) str.push("Chat");
            if (rights & 32768) str.push("Desinstalar");
            if (rights & 131072) str.push("Comandos");
            if (rights & 262144) str.push("Reset/Apagado");
            if (rights & 524288) str.push("Intercambio");
            if (rights & 1048576) str.push("Detalles");
            if (rights & 2097152) str.push("Relé");
            if (str.length == 0) return "Sin Permisos";
            return str.join(', ');
        }

        // Run commands on current device
        function runDeviceCmd(nodeid) { if (xxdialogMode) return; d2runCommandDialog({ nodeids: [nodeid ? decodeURIComponent(nodeid) : currentNode._id] }); }

        function writeDeviceEvent(nodeid) {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', "Agregar Evento del Dispositivo", '<textarea id=d2devEvent style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea><span style=font-size:10px>' + "Esto agregará una entrada al registro de eventos de este dispositivo." + '<span>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                writeDeviceEventEx(3, nodeid);
            });
            Q('d2devEvent').focus();
        }

        function writeDeviceEventEx(buttons, tag) { meshserver.send({ action: 'setDeviceEvent', nodeid: decodeURIComponent(tag), msg: encodeURIComponentEx(Q('d2devEvent').value) }); }

        function showNotes(readonly, noteid) {
            if (xxdialogMode) return;
            if (noteid == null) { noteid = encodeURIComponentEx('p' + userinfo._id); }
            var x = '<textarea id=d2devNotes ro=' + readonly + ' noteid=' + noteid + ' readonly class="form-control" style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea>';
            if (noteid.startsWith('node%2F%2F')) { x += '<span style=font-size:10px>' + "Las notas del grupo de dispositivos pueden ser vistas y modificadas por otros administradores de grupos de dispositivos." + '<span>'; }
            if (showNotesPanel === 'true') { x += ' <span style=font-size:10px><a target=_blank href=\'https://www.markdownguide.org/cheat-sheet/\'>' + "Sintaxis Markdown admitida" + '</a></span>'; }
            setModalContent('xxAddAgent', "Notas", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                showNotesEx(2, noteid);
            });

            meshserver.send({ action: 'getNotes', id: decodeURIComponent(noteid) });
        }

        function showNotesEx(buttons, tag) {
            Q('notesPanelArea').innerHTML = (marked && DOMPurify) ? DOMPurify.sanitize(marked.parse(Q('d2devNotes').value, { breaks: true }), { USE_PROFILES: { html: true } }) : Q('d2devNotes').value;
            meshserver.send({ action: 'setNotes', id: decodeURIComponent(tag), notes: encodeURIComponentEx(Q('d2devNotes').value) });
            if ((showNotesPanel === 'true') && Q('d2devNotes').value != '') { QV('notesPanel', true); } else { QV('notesPanel', false); }
        }

        function openIpKvmRemoteControl(nodeid) {
            if (xxdialogMode) return;
            var nid = decodeURIComponent(nodeid).split('/')[2];
            safeNewWindow('/ipkvm.ashx/' + nid + '/', 'ipkvm:' + nid);
        }

        function deviceChat(e) {
            if (xxdialogMode) return;
            var url = '/messenger?id=meshmessenger/' + encodeURIComponentEx(currentNode._id) + '/' + encodeURIComponentEx(userinfo._id) + '&title=' + currentNode.name;
            if (serverinfo.domainsuffix != '') { url = '/' + serverinfo.domainsuffix + url; }
            if ((authCookie != null) && (authCookie != '')) { url += '&auth=' + authCookie; }
            if ((currentNode.pmt == 1) && ((features2 & 2) != 0)) { url += '&pmt=1'; } // Push messaging is possible for this device
            if (e && (e.shiftKey == true)) {
                safeNewWindow(url, 'meshmessenger:' + currentNode._id);
            } else {
                safeNewWindow(url, 'meshmessenger:' + currentNode._id, 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=400,height=560');
            }
            meshserver.send({ action: 'meshmessenger', nodeid: decodeURIComponent(currentNode._id) });
        }

        function altDeviceChat(e, i) {
            if (xxdialogMode) return;
            var url = serverinfo.altmessenging[i].url.split('{0}').join(currentNode._id.split('/')[2]).split('{1}').join(currentNode._id.split('/')[2]).split('{2}').join(currentNode._id.split('/')[2]).split('{3}').join(currentNode._id.split('/')[2]);
            var userid1 = encodeURIComponentEx(userinfo._id.split('/')[2]); // userid
            var userid2 = encodeURIComponentEx(userinfo._id.split('/').join('-')); // user-domain-userid
            var userid3 = userid1, userid4 = userid2;
            if (userinfo.realname != null) {
                userid3 = encodeURIComponentEx(userinfo.realname.split(' ').join('')); // real name with no empty spaces
                userid4 = encodeURIComponentEx(userinfo.realname.split(' ').join('-')); // real name with - instead of spaces
            }
            url = url.split('{4}').join(userid1).split('{5}').join(userid2).split('{6}').join(userid3).split('{7}').join(userid4);
            var localurl = url;
            if (typeof serverinfo.altmessenging[i].localurl == 'string') {
                localurl = serverinfo.altmessenging[i].localurl.split('{0}').join(currentNode._id.split('/')[2]).split('{1}').join(currentNode._id.split('/')[2]).split('{2}').join(currentNode._id.split('/')[2]).split('{3}').join(currentNode._id.split('/')[2]);
                localurl = localurl.split('{4}').join(userid1).split('{5}').join(userid2).split('{6}').join(userid3).split('{7}').join(userid4);
            }
            if (url != '') { meshserver.send({ action: 'msg', type: 'openUrl', nodeid: currentNode._id, url: url }); }
            if (localurl != '') { safeNewWindow(localurl, 'altmessenger:' + currentNode._id, 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=400,height=560'); }
        }

        function deviceToggleBackground() {
            if (xxdialogMode) return;
            meshserver.send({ action: 'msg', type: 'deskBackground', nodeid: currentNode._id, op: 1 }); // Toggle desktop background image
        }

        function deviceUrlFunction() {
            if (xxdialogMode) return;
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Abrir Página en Dispositivo", '<input id=d2devurl placeholder="http://server.com" style=width:100%;overflow-y:scroll onkeyup=deviceUrlFunctionValidate() onchange=deviceUrlFunctionValidate()></input>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', deviceUrlFunctionEx);

            Q('d2devurl').focus();
            deviceUrlFunctionValidate();
        }

        function deviceUrlFunctionValidate() {
            var x = Q('d2devurl').value.toLowerCase();
            QE('idx_dlgOkButton', ((x.startsWith('http://') && (x.length > 7)) || (x.startsWith('https://') && (x.length > 8))));
        }

        function deviceUrlFunctionEx() {
            meshserver.send({ action: 'msg', type: 'openUrl', nodeid: currentNode._id, url: Q('d2devurl').value });
        }

        function deviceMessageFunction() {
            if (xxdialogMode) return;
            var x = '<div style=margin-bottom:4px>' + "Mostrar un cuadro de mensaje en el dispositivo remoto." + '</div>';
            x += '<textarea id=d2devMessage style=width:100%;height:80px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px></textarea>';
            x += '<select style=width:100% id=d2devTimeout>';
            x += '<option value=2 selected>' + "Show for 2 Minutes (Default)" + '</option>';
            x += '<option value=10>' + "Show for 10 minutes" + '</option>';
            x += '<option value=30>' + "Show for 30 minutes" + '</option>';
            x += '<option value=60>' + "Show for 60 minutes" + '</option>';
            x += '<option value=0>' + "Mostrar mensaje hasta que el usuario lo descarte" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Mensaje del dispositivo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', deviceMessageFunctionEx);
            Q('d2devMessage').focus();
        }

        function deviceMessageFunctionEx() {
            var title = decodeURIComponent('{{{extitle}}}'), timeout = parseFloat(Q('d2devTimeout').value);
            if (title == '') { title = "MeshCentral"; }
            if (isNaN(timeout)) { timeout = 2; }
            if (currentNode.pmt == 1) {
                meshserver.send({ action: 'pushmessage', nodeid: currentNode._id, title: title, msg: Q('d2devMessage').value });
            } else {
                meshserver.send({ action: 'msg', type: 'messagebox', nodeid: currentNode._id, title: title, msg: Q('d2devMessage').value, timeout: (timeout * 60000) });
            }
        }

        function deskClipboardInFunction() {
            if (desktop == null || desktop.State != 3) return;
            if (desktop.m.getClipboard) {
                var text = desktop.m.getClipboard();
                if ((text != null) && (navigator.clipboard != null)) { navigator.clipboard.writeText(text).then(function () { }).catch(function (err) { console.log(err); }) } // Put remote clipboard data into our clipboard
            } else {
                meshserver.send({ action: 'msg', type: 'getclip', nodeid: currentNode._id, tag: 2 });
            }
        }

        // Called to lock or unlock remote desktop user input
        function deskInputLockFunction(value) {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            setModalContent('xxAddAgent', "Bloqueo de Entrada Remota", (value == 1) ? "¿Bloquear el mouse y el teclado del usuario remoto?" : "¿Desbloquear el mouse y el teclado del usuario remoto?");
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { try { desktop.m.SendRemoteInputLock(value); } catch (ex) { } });
        }

        function deskClipboardOutFunction() {
            if ((navigator.clipboard != null) && (navigator.clipboard.readText != null)) {
                try {
                    navigator.clipboard.readText().then(function (text) {
                        if (desktop.m.setClipboard) { desktop.m.setClipboard(text); } else { meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text }); }
                    }).catch(function (err) { console.log(err); });
                } catch (ex) { console.log(ex); }
            }
            return true;
        }

        function deskRefreshFunction() {
            if (desktop == null || desktop.State != 3) return;
            desktop.m.SendRefresh();
        }

        // Adapted from Classic Mobile's deskChangeMouseButton()
        function deskMobileToggleZoom() {
            _mobileSetZoom(_mobileZoomMode === 'native' ? 'fit' : 'native');
        }

        function deskToggleRightClickMode() {
            if (xxdialogMode || desktop == null) return;
            desktop.m.SwapMouse = !desktop.m.SwapMouse;
        }

        var _deskMobilePanelStaticHTML = null;
        function deskToggleMobileActions() {
            var panel = Q('deskMobileActionsPanel'), backdrop = Q('deskMobileActionsBackdrop');
            if (!panel) return;
            var show = (panel.style.display === 'none');
            if (show) {
                if (document.body.classList.contains('fulldesk')) {
                    // Fullscreen mode: save static HTML if not yet saved, then rebuild
                    if (_deskMobilePanelStaticHTML === null) _deskMobilePanelStaticHTML = panel.innerHTML;
                    _buildFullscreenPanel(panel);
                } else {
                    // Normal mode: restore static HTML if fullscreen had replaced it
                    if (_deskMobilePanelStaticHTML !== null) {
                        panel.innerHTML = _deskMobilePanelStaticHTML;
                        _deskMobilePanelStaticHTML = null;
                        // Re-apply current monitor selection since saved HTML may be stale
                        if (_deskLastSelDisplay !== null) {
                            ['deskMobileMonitorIcons', 'DeskMonitorSelectionSpan'].forEach(function(id) {
                                var container = Q(id);
                                if (!container) return;
                                container.querySelectorAll('[id^="DeskMonitorSelectionX"]').forEach(function(el) {
                                    var idx = parseInt(el.id.replace('DeskMonitorSelectionX', ''));
                                    if (idx === _deskLastSelDisplay) { el.classList.remove('gray'); } else { el.classList.add('gray'); }
                                });
                            });
                        }
                    }
                    // Update dynamic labels to reflect current state
                    var rc = Q('deskRightClickItem');
                    if (rc) {
                        var active = desktop && desktop.m && desktop.m.SwapMouse;
                        rc.innerHTML = '<i class="fa-solid fa-hand-pointer fa-fw me-2 text-primary"></i>'
                            + (active ? 'Disable Right-click Mode' : 'Enable Right-click Mode');
                    }
                    var tp = Q('deskTrackpadItem');
                    if (tp) {
                        tp.innerHTML = '<i class="fa-solid fa-computer-mouse fa-fw me-2 text-primary"></i>'
                            + (trackpadMode ? 'Disable Trackpad Mode' : 'Enable Trackpad Mode');
                    }
                    var ki = Q('deskKbdItem');
                    if (ki) {
                        ki.innerHTML = '<i class="fa-solid fa-keyboard fa-fw me-2 text-primary"></i>' + "Teclado en pantalla"
                            + ' <i class="fa-solid ' + (_mobileKbdEnabled ? 'fa-toggle-on text-success' : 'fa-toggle-off text-secondary') + ' ms-1"></i>';
                    }
                }
            }
            QV('deskMobileActionsPanel', show);
            QV('deskMobileActionsBackdrop', show);
            // Toggle floating button icon: when closed -> ✕ when open
            var fsBtn = Q('deskFsBtn');
            if (fsBtn) fsBtn.innerHTML = show ? '<i class="fa-solid fa-xmark"></i>' : '<i class="fa-solid fa-bars"></i>';
        }

        function _buildFullscreenPanel(panel) {
            var inputOn  = Q('DeskControl') && Q('DeskControl').checked;
            var swap     = desktop && desktop.m && desktop.m.SwapMouse;
            var trackpad = (typeof trackpadMode !== 'undefined') && trackpadMode;
            var s   = 'font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)';
            var cls = 'd-block px-3 py-2 text-decoration-none border-bottom';
            var end = 'd-block px-3 py-2 text-decoration-none'; // last item, no border
            function row(c, icon, label, action, extra) {
                return '<a class="' + c + '" style="' + s + '" onclick="' + action + '">'
                     + '<i class="fa-solid ' + icon + ' fa-fw me-2 ' + (extra || 'text-primary') + '"></i>'
                     + label + '</a>';
            }
            var monitorRow = '';
            if (_deskLastDisplays) {
                var icons = '', count = 0;
                for (var i in _deskLastDisplays) {
                    count++;
                    var str = _deskLastDisplays[i], allDisplays = 1;
                    if (str == 'All Displays') { allDisplays = 2; }
                    var mt = 'id=DeskMonitorSelectionX' + i + ' title="' + EscapeHtml(str) + '" onclick=deskSetDisplay(' + i + ') role=button';
                    var gray = (_deskLastSelDisplay == i) ? '' : ' gray';
                    if (allDisplays == 2) {
                        icons += '<span ' + mt + ' class="fa-layers fa-fw' + gray + '"><i class="fa-solid fa-desktop"></i><i class="fa-solid fa-square" data-fa-transform="shrink-12 left-3 up-3"></i><i class="fa-solid fa-square" data-fa-transform="shrink-12 right-3 up-3"></i></span>';
                    } else {
                        icons += '<i ' + mt + ' class="fa-fw fa-solid fa-desktop' + gray + '"></i>';
                    }
                }
                if (count > 1) monitorRow = '<div class="d-flex align-items-center px-3 py-2 border-bottom" style="' + s + '"><i class="fa-solid fa-desktop fa-fw me-2 text-primary"></i>Select Monitor<span class="ms-auto d-flex gap-2" style="font-size:20px">' + icons + '</span></div>';
            }
            var modChips = '<div class="d-flex gap-2 px-3 py-2 border-bottom" style="background:var(--bs-body-bg,#fff)">'
                + '<button type="button" id="deskModCtrl"  class="desk-mod-key' + (_mobileModifiers.ctrl  ? ' armed' : '') + '" onclick="toggleMobileModifier(\'ctrl\')" >' + "CTRL"  + '</button>'
                + '<button type="button" id="deskModShift" class="desk-mod-key' + (_mobileModifiers.shift ? ' armed' : '') + '" onclick="toggleMobileModifier(\'shift\')">' + "SHIFT" + '</button>'
                + '<button type="button" id="deskModAlt"   class="desk-mod-key' + (_mobileModifiers.alt   ? ' armed' : '') + '" onclick="toggleMobileModifier(\'alt\')" >' + "ALT"   + '</button>'
                + '</div>';
            panel.innerHTML = modChips +
                row(cls, 'fa-compress',         "Salir de pantalla completa",                          'deskToggleFull();deskCloseMobileActions()',                       'text-danger') +
                monitorRow +
                row(cls, (_mobileZoomMode==='native' ? 'fa-compress-arrows-alt' : 'fa-expand-arrows-alt'),
                    (_mobileZoomMode==='native' ? "Escalar para ajustar" : "Tamaño nativo (desplazable)"),  'deskMobileToggleZoom();deskCloseMobileActions()') +
                row(cls, 'fa-keyboard',          'Input <i class="fa-solid ' + (inputOn?'fa-toggle-on text-success':'fa-toggle-off text-secondary') + ' ms-1"></i>', 'Q("DeskControl").click();deskCloseMobileActions()') +
                row(cls, 'fa-keyboard',          "Teclado en pantalla" + ' <i class="fa-solid ' + (_mobileKbdEnabled?'fa-toggle-on text-success':'fa-toggle-off text-secondary') + ' ms-1"></i>', 'toggleMobileKeyboard();deskCloseMobileActions()') +
                row(cls, 'fa-computer-mouse',    (trackpad ? "Deshabilitar modo touchpad" : "Habilitar modo touchpad"),  'deskToggleTrackpad();deskCloseMobileActions()') +
                row(cls, 'fa-hand-pointer',      (swap ? "Deshabilitar modo clic derecho" : "Habilitar modo clic derecho"),   'deskToggleRightClickMode();deskCloseMobileActions()') +
                row(cls, 'fa-font',              "Escribir texto",                                'showDeskType();deskCloseMobileActions()') +
                row(cls, 'fa-terminal',          'Ctrl + Alt + Del',                        'if(desktop&&desktop.m)desktop.m.sendcad();deskCloseMobileActions()') +
                row(cls, 'fa-keyboard',          'Send ESC',                                 'sendDeskEsc();deskCloseMobileActions()') +
                row(cls, 'fa-sliders',           "Atajos de teclado",                       'deskCustomizeKeys();deskCloseMobileActions()') +
                row(cls, 'fa-paste',             "Portapapeles",                               'showDeskClip();deskCloseMobileActions()') +
                row(cls, 'fa-clipboard',         "Subir portapapeles",                         'deskClipboardOutFunction();deskCloseMobileActions()') +
                row(cls, 'fa-rotate-right',      "Gira a la Derecha",                             'drotate(1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate-left',       "Girar a la Izquierda",                              'drotate(-1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate',            "Actualizar escritorio",                          'deskRefreshFunction();deskCloseMobileActions()') +
                row(cls, 'fa-gear',              "Configuración del escritorio",                         'showDesktopSettings();deskCloseMobileActions()') +
                row(end, 'fa-plug-circle-xmark', "Desconectar",                               'connectDesktop(null,0);deskCloseMobileActions()',                 'text-danger');
        }
        function deskCloseMobileActions() {
            QV('deskMobileActionsPanel', false);
            QV('deskMobileActionsBackdrop', false);
            var fsBtn = Q('deskFsBtn');
            if (fsBtn) fsBtn.innerHTML = '<i class="fa-solid fa-bars"></i>';
        }

        function deviceToastFunction() {
            if (xxdialogMode) return;
            var x = '<select id=d2deviceop style=width:100%;margin-bottom:4px class=form-select><option value=2>' + "Notificación Emergente Temporal" + '</option><option value=1>' + "Cuadro de Mensaje" + '</option><option value=3>' + "Alert Box" + '</option></select>';
            x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Título" + '" style=width:100%;box-sizing:border-box;margin-bottom:4px class=form-control/>';
            x += '<textarea id=d2notifyMsg style=background-color:#fcf3cf;width:100%;height:140px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px class=form-control></textarea>';
            x += '<select style=width:100% id=d2notifyTimeout class=form-select>';
            x += '<option disabled value="">Only Applicable to Message Box Notifications</option>';
            x += '<option value=2 selected>' + "Show for 2 Minutes (Default)" + '</option>';
            x += '<option value=10>' + "Show for 10 minutes" + '</option>';
            x += '<option value=30>' + "Show for 30 minutes" + '</option>';
            x += '<option value=60>' + "Show for 60 minutes" + '</option>';
            x += '<option value=0>' + "Mostrar mensaje hasta que el usuario lo descarte" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Notificar Dispositivo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', deviceToastFunctionEx);
            Q('d2notifyMsg').focus();
        }

        function deviceToastFunctionEx() {
            var op = Q('d2deviceop').value, title = Q('dp2notifyTitle').value, msg = Q('d2notifyMsg').value, timeout = parseFloat(Q('d2notifyTimeout').value);
            if (msg.length == 0) return;
            if (title == '') { title = decodeURIComponent('{{{extitle}}}'); }
            if (title == '') { title = "MeshCentral"; }
            if (isNaN(timeout)) { timeout = 2; }
            if (op == 1) { // MessageBox
                meshserver.send({ action: 'msg', type: 'messagebox', nodeid: currentNode._id, title: title, msg: msg, timeout: (timeout * 60000) });
            } else if (op == 2) { // Toast
                meshserver.send({ action: 'toast', nodeids: [currentNode._id], title: title, msg: msg });
            } else if (op == 3) { // Old Style MessageBox
                meshserver.send({ action: 'msg', type: 'alertbox', nodeid: currentNode._id, title: title, msg: msg });
            }
        }

        // Lock desktop
        function deviceLockFunction() {
            if ((xxdialogMode == null) && (desktop != null) && (desktop.contype == 1)) {
                setModalContent('xxAddAgent', "Bloquear Escritorio", "¿Bloquear el escritorio del usuario?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { if ((desktop != null) && (desktop.contype == 1)) { desktop.sendCtrlMsg('{"ctrlChannel":"102938","type":"lock"}'); } });
            }
        }

        function showShareDevice() {
            if (xxdialogMode) return;
            var rights = GetNodeRights(currentNode);
            var y = '', x = "Crea un enlace que permite a un invitado sin una cuenta controlar remotamente este dispositivo por un tiempo limitado." + '<br /><br />';
            x += addHtmlFormFloating("Nombre de Invitado", '<input id=d2inviteName class="form-control" maxlength=128 type=text onkeyup=showShareDeviceValidate() />');
            var deskFull = '<option value=2>' + "Escritorio" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x100) != 0)) { deskFull = ''; }
            var fullTerm = '<option value=1>' + "Terminal" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x200) != 0)) { fullTerm = ''; }
            var fullFiles = '<option value=4>' + "Archivos" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x400) != 0)) { fullFiles = ''; }
            var deskFiles = '<option value=5>' + "Escritorio + Archivos" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x500) != 0)) { deskFiles = ''; }
            var termFiles = '<option value=6>' + "Terminal + Archivos" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x600) != 0)) { termFiles = ''; }
            var allFeatures = '<option value=7>' + "Escritorio + Terminal + Archivos" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x700) != 0)) { allFeatures = ''; }
            var httpFeature = '';
            if (webRelayPort != 0) {
                httpFeature = '<option value=8>' + "HTTP" + '</option><option value=9>' + "HTTPS" + '</option>';
                if ((rights != 0xFFFFFFFF) && ((rights & 8) != 0)) { httpFeature = ''; }
            }

            var y = '', z = '';
            if ((currentNode.agent.caps & 1) == 1) { y += (deskFull + '<option value=3>' + "Escritorio, Solo vista" + '</option>'); } // Agent is desktop capable
            if ((currentNode.agent.caps & 2) == 2) { y += fullTerm; } // Agent is terminal capable
            if ((currentNode.agent.caps & 4) == 4) { y += fullFiles; } // Agent is files capable
            if ((currentNode.agent.caps & 5) == 5) { y += deskFiles; } // Agent is desktop + files capable
            if ((currentNode.agent.caps & 6) == 6) { y += termFiles; } // Agent is terminal + files capable
            if ((currentNode.agent.caps & 7) == 7) { y += allFeatures; } // Agent is desktop + terminal + files capable
            y += httpFeature;

            x += addHtmlFormFloating("Tipo", '<select id=d2shareType class="form-select" onchange=showShareDeviceValidate()>' + y + '</select>');
            var options = { 1: "1 minuto", 5: "5 minutos", 10: "10 minutos", 15: "15 minutos", 30: "30 minutos", 45: "45 minutos", 60: "60 minutos", 120: "2 horas", 240: "4 horas", 480: "8 horas", 720: "12 horas", 960: "16 horas", 1440: "24 horas", 2880: "2 días", 5760: "4 dias", 0: "Ilimitado" }
            y = '';
            for (var i in options) {
                if ((serverinfo.guestdevicesharingmaxtime == null) || ((i > 0) && (i <= serverinfo.guestdevicesharingmaxtime))) {
                    y += '<option value=' + i + '>' + options[i] + '</option>';
                }
                if ((i > 0) && (i <= 720) && ((serverinfo.guestdevicesharingmaxtime == null) || ((i > 0) && (i <= serverinfo.guestdevicesharingmaxtime)))) {
                    z += '<option value=' + i + '>' + options[i] + '</option>';
                }
            }
            x += addHtmlFormFloating("Validez", '<select id=d2timeRange class="form-select" onchange=showShareDeviceValidate()><option value=0>' + "Comenzando ahora" + '</option><option value=1>' + "Intervalo de tiempo" + '</option><option value=2>' + "Recurrente diariamente" + '</option><option value=3>' + "Semanal recurrente" + '</option></select>');
            x += '<div id=d2modenow>';
            x += addHtmlFormFloating("Tiempo de Expiración", '<select id=d2inviteExpire class="form-select">' + y + '</select>');
            x += '</div><div id=d2moderange style=display:none>';
            x += addHtmlFormFloating("Inicio de rango de tiempo", '<input id=d2timeRangeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Seleccionad fecha y hora..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Final del rango de tiempo", '<input id=d2timeRangeEndSelector class="flatpickr form-select" type="text" placeholder="' + "Seleccionad fecha y hora..." + '" data-id="altinput">');
            x += '</div><div id=d2moderecurring style=display:none>';
            x += addHtmlFormFloating("Tiempo de Inicio", '<input id=d2timeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Seleccionad fecha y hora..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Duración", '<select id=d2inviteDuration class="form-select">' + z + '</select>');
            x += '</div>';
            if (currentNode.agent.caps & 1) { x += '<div id=d2userConsentSelector>' + addHtmlFormFloating("Consentimiento del Usuario", '<select id=d2userConsent class="form-select"><option value=0>' + "Sólo Notificar" + '<option value=1>' + "Solicitar consentimiento" + '</option><option value=2>' + "No Consent" + '</option></select>') + '</div>'; }
            x += '<div id=d2httpPortSelector>' + addHtmlFormFloating("Puerto", '<input id=d2httpPort class="form-control" value=80 onkeyup=showShareDeviceValidate()></input>') + '</div>';
            x += '<div id=d2httpsPortSelector>' + addHtmlFormFloating("Puerto", '<input id=d2httpsPort class="form-control" value=443 onkeyup=showShareDeviceValidate()></input>') + '</div>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Compartir Dispositivo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showShareDeviceEx('', xxdialogTag));

            showShareDeviceValidate();
            var tomorrow = new Date();
            tomorrow.setDate(tomorrow.getDate() + 1);
            var rangeStartTime = flatpickr('#d2timeRangeStartSelector', {
                enableTime: true, minDate: new Date(), defaultDate: new Date(), minuteIncrement: 1,
                plugins: [new confirmDatePlugin({})],
                onChange: function(selectedDates, dateStr, instance) {
                    rangeEndTime.set('minDate', dateStr);
                    if (rangeEndTime.selectedDates[0] && rangeEndTime.selectedDates[0] < selectedDates[0]) {
                        rangeEndTime.clear();
                    }
                }
            });
            var rangeEndTime = flatpickr('#d2timeRangeEndSelector', { enableTime: true, minDate: tomorrow, defaultDate: tomorrow, minuteIncrement: 1, plugins: [new confirmDatePlugin({})] });
            var startTime = flatpickr('#d2timeStartSelector', { enableTime: true, minDate: new Date(), defaultDate: new Date(), minuteIncrement: 1, plugins: [new confirmDatePlugin({})] });
            xxdialogTag = { rangeStart: rangeStartTime, rangeEnd: rangeEndTime, start: startTime };
        }

        function showShareDeviceValidate() {
            if (currentNode.agent.caps & 1) { QV('d2userConsentSelector', Q('d2shareType').value < 8); }
            QV('d2httpPortSelector', Q('d2shareType').value == 8);
            QV('d2httpsPortSelector', Q('d2shareType').value == 9);
            QV('d2modenow', Q('d2timeRange').value == 0);
            QV('d2moderange', Q('d2timeRange').value == 1);
            QV('d2moderecurring', Q('d2timeRange').value >= 2);
            var ok = true;
            if (Q('d2shareType').value == 8) { var port = parseInt(Q('d2httpPort').value); if ((Q('d2httpPort').value != port) || (port < 1) || (port > 65535)) { ok = false; } }
            if (Q('d2shareType').value == 9) { var port = parseInt(Q('d2httpsPort').value); if ((Q('d2httpsPort').value != port) || (port < 1) || (port > 65535)) { ok = false; } }
            if (Q('d2inviteName').value.trim().length == 0) { ok = false; }
            QE('idx_dlgOkButton', ok);
        }

        function showShareDeviceEx(b, tag) {
            var consent = 0, p = parseInt(Q('d2shareType').value), viewOnly = false, q = 0;
            if (p == 8) { meshserver.send({ action: 'createDeviceShareLink', nodeid: currentNode._id, guestname: Q('d2inviteName').value.trim(), p: 8, expire: parseInt(Q('d2inviteExpire').value), port: parseInt(Q('d2httpPort').value), consent: 0 }); return; }
            if (p == 9) { meshserver.send({ action: 'createDeviceShareLink', nodeid: currentNode._id, guestname: Q('d2inviteName').value.trim(), p: 16, expire: parseInt(Q('d2inviteExpire').value), port: parseInt(Q('d2httpsPort').value), consent: 0 }); return; }
            if (p == 3) { viewOnly = true; }
            var q = [0, 1, 2, 2, 4, 6, 5, 7][p]; // Protocol flags: 1 = Terminal, 2 = Desktop, 4 = Files, 8 = HTTP, 16 = HTTPS.

            if (q & 1) {
                consent |= 0x0002; // Terminal notify
                if ((currentNode.agent.caps & 1) && (Q('d2userConsent').value == 1)) { consent |= 0x0010; } // Terminal prompt for user consent
                if ((currentNode.agent.caps & 1) && (Q('d2userConsent').value == 2)) { consent = 0; } // Terminal with no user consent
            }
            if (q & 2) {
                consent |= 0x0041; // Desktop connection toolbar + Desktop notify
                if ((currentNode.agent.caps & 1) && (Q('d2userConsent').value == 1)) { consent |= 0x0008; } // Desktop prompt for user consent
                if ((currentNode.agent.caps & 1) && (Q('d2userConsent').value == 2)) { consent = 0; } // Desktop with no user consent
            }
            if (q & 4) {
                consent |= 0x0004; // Files notify
                if ((currentNode.agent.caps & 1) && (Q('d2userConsent').value == 1)) { consent |= 0x0020; } // Files prompt for user consent
                if ((currentNode.agent.caps & 1) && (Q('d2userConsent').value == 2)) { consent = 0; } // Files prompt for user consent
            }

            if (Q('d2timeRange').value == 0) {
                meshserver.send({ action: 'createDeviceShareLink', nodeid: currentNode._id, guestname: Q('d2inviteName').value.trim(), p: q, expire: parseInt(Q('d2inviteExpire').value), consent: consent, viewOnly: viewOnly });
            } else if (Q('d2timeRange').value == 1) {
                meshserver.send({ action: 'createDeviceShareLink', nodeid: currentNode._id, guestname: Q('d2inviteName').value.trim(), p: q, start: Math.floor(tag.rangeStart.selectedDates[0].getTime() / 1000), end: Math.floor(tag.rangeEnd.selectedDates[0].getTime() / 1000), consent: consent, viewOnly: viewOnly });
            } else {
                meshserver.send({ action: 'createDeviceShareLink', nodeid: currentNode._id, guestname: Q('d2inviteName').value.trim(), p: q, start: Math.floor(tag.start.selectedDates[0].getTime() / 1000), expire: parseInt(Q('d2inviteDuration').value), recurring: Q('d2timeRange').value - 1, consent: consent, viewOnly: viewOnly });
            }
            return false;
        }

        function deviceActionFunction() {
            if (xxdialogMode) return;
            var rights = GetNodeRights(currentNode), count = 0;
            var x = "Selecciona una operación para realizar en este dispositivo." + '<br /><br />';
            var y = '<select id=d2deviceop onchange=deviceActionFunctionValidate() class=form-select>';
            var z = '';
            if ((currentNode.agent != null) && (currentNode.agent.id == 14)) {
                if (((currentNode.conn & 1) != 0) && ((rights & 8) != 0)) {
                    count++;
                    y += '<option value=400>' + "Destello" + '</option>';
                    y += '<option value=401>' + "Vibrar" + '</option>';
                    z += '<div id=d2devicetimediv>' + addHtmlFormFloating("Tiempo", '<select id=d2devicetime class=form-select><option value=1000>' + "1 segundo" + '</option><option value=5000>' + "5 segundos" + '</option><option value=10000>' + "10 segundos" + '</option></select>') + '</div>';
                }
            } else {
                if ((rights & 64) != 0) { count++; y += '<option value=100>' + "Despertar" + '</option>'; } // Wake-up permission
                if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Ejecutar Comandos" + '</option>'; } // Remote command permission
                if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Suspender" + '</option><option value=3>' + "Reiniciar" + '</option><option value=2>' + "Apagar" + '</option>'; }
                if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Enviar Mensaje MQTT" + '</option>'; }
                if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
                    count++;
                    y += '<option value=310>' + "Restablecimiento de Intel&reg; AMT" + '</option>';
                    y += '<option value=308>' + "Apagado de Intel&reg; AMT" + '</option>';
                    if ((xxcurrentView == 11) || (xxcurrentView == 12)) { // Only show these options on terminal or desktop tabs
                        y += '<option value=312>' + "Intel&reg; AMT Reinicio a BIOS" + '</option>';
                        y += '<option value=311>' + "Intel&reg; AMT Encender a BIOS" + '</option>';
                        y += '<option value=315>' + "Intel&reg; AMT Power on to PXE" + '</option>';
                        y += '<option value=316>' + "Intel&reg; AMT Reset to PXE" + '</option>';
                    }
                }
                if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
                    count++;
                    y += '<option value=302>' + "Encendido de Intel&reg; AMT" + '</option>';
                }
                if (((xxcurrentView == 11) || (xxcurrentView == 12)) && (getNodeAmtVersion(currentNode) >= 15) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF) && ((features & 0x00000400) == 0)) { count++; y += '<option value=107>' + "Recuperación Intel&reg; AMT en un clic" + '</option>'; } // CIRA (2) or AMT (4) connected
                if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Desinstalar Agente" + '</option>'; }
            }
            y += '</select>';
            x += addHtmlFormFloating("Operación", y);
            if (count == 0) { x = "Actualmente no hay acciones disponibles para este dispositivo."; }
            xxdialogButtons = (count == 0) ? 2 : 3; xxdialogTag = x + z;
            setModalContent('xxAddAgent', "Acción del Dispositivo", xxdialogTag);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', deviceActionFunctionEx);
            if (count > 0) { deviceActionFunctionValidate(); }
        }

        function deviceActionFunctionValidate() {
            var op = Q('d2deviceop').value;
            try { QV('d2devicetimediv', (op == 400) || (op == 401)); } catch (ex) { }
        }

        function deviceActionFunctionEx() {
            var op = Q('d2deviceop').value;
            if (op == 100) {
                // Device wake
                meshserver.send({ action: 'wakedevices', nodeids: [currentNode._id] });
                return true;
            } else if (op == 103) {
                // Send MQTT Message
                p10showSendMqttMsgDialog([currentNode._id]);
            } else if (op == 104) {
                // Uninstall agent
                p10showSendUninstallAgentDialog([currentNode._id]);
            } else if (op == 106) {
                // Run commands
                var wintype = false, linuxtype = false;
                if (currentNode.agent) { if (isWindowsNode(n)) { wintype = true; } else { linuxtype = true; } }
                if ((wintype == true) || (linuxtype == true)) {
                    var x = "Ejecutar comandos en los dispositivos seleccionados." + '<br />';
                    if (wintype == true) {
                        x += '<select id=d2cmdtype style=width:100%;margin-bottom:4px;margin-top:4px> class=form-select>';
                        x += '<option value=1>' + "Línea de Comandos de Windows" + '</option><option value=2>' + "Windows PowerShell" + '</option>';
                        if (linuxtype == true) { x += '<option value=3>' + "Shell de comandos Linux/BSD/macOS" + '</option>'; }
                        x += '</select>';
                    }
                    x += '<select id=d2cmduser style=width:100%;margin-bottom:4px><option value=0>' + "Ejecutar como agente" + '</option><option value=1>' + "Ejecutar como usuario, agente si no hay usuario" + '</option><option value=2>' + "Debe ejecutarse como usuario" + '</option></select>';
                    x += '<textarea id=d2runcmd style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea>';
                    setModalContent('xxAddAgent', "Ejecutar Comandos", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deviceRunCmdsFunctionEx());
                    Q('d2runcmd').focus();
                    //QE('idx_dlgOkButton', true);
                }
            } else if (op == 107) {
                // Intel AMT One Click Recovery (OCR)
                Q('d3localmodeform').action = 'oneclickrecovery.ashx';
                Q('d3auth').value = authCookie;
                Q('d3filter').value = '.efi';
                Q('d3attrib').value = currentNode._id;
                setModalContent('xxAddAgent', "Recuperación Intel&reg; AMT en un clic", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deviceActionOneClickRecovery());
                d3init();
            } else if (op == 302) { // Intel AMT power on
                setModalContent('xxAddAgent', "Operación de energía Intel&reg; AMT", "¿Realizar el encendido de Intel&reg; AMT?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) }); });
            } else if (op == 308) { // Intel AMT power off
                setModalContent('xxAddAgent', "Operación de energía Intel&reg; AMT", "Perform Intel&reg; AMT power off?<br><br><b>NOTE: If there is an active AMT session, then power off command will be rejected, so you must disconnect from the AMT session first!</b>");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) }); });
            } else if (op == 310) { // Intel AMT reset
                setModalContent('xxAddAgent', "Operación de energía Intel&reg; AMT", "¿Realizar reinicio de Intel&reg; AMT?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) }); });
            } else if (op == 311) { // Intel AMT power on to BIOS
                setModalContent('xxAddAgent', "Operación de energía Intel&reg; AMT", "Ejecutar accion de Encendido Intel&reg; AMT en BIOS?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) + ((xxcurrentView == 12) ? 2 : 0) }); });
            } else if (op == 312) { // Intel AMT reset to BIOS
                setModalContent('xxAddAgent', "Operación de energía Intel&reg; AMT", "Ejecutar accion de Reinicio Intel&reg; AMT en BIOS?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) + ((xxcurrentView == 12) ? 2 : 0) }); });
            } else if ((op == 400) || (op == 401)) {
                // Flash / vibrate
                meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op), time: parseInt(Q('d2devicetime').value) });
                return true;
            } else {
                // Power operation
                meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) });
                return true;
            }
            return false;
        }

        function deviceActionOneClickRecovery() {
            var mode = Q('d3uploadMode').value;
            if (mode == 1) {
                // Boot using local disk image file
                Q('d3submit').click();
            } else {
                // Boot using server image file
                var files = d3getFileSel();
                if (files.length == 1) { meshserver.send({ action: 'oneclickrecovery', nodeids: [Q('d3attrib').value], type: 'diskimage', path: d3filetreelocation.join('/') + '/' + files[0] }); }
            }
        }

        function deviceRunCmdsFunctionEx() {
            var type = 3;
            try { type = parseInt(Q('d2cmdtype').value); } catch (ex) { }
            meshserver.send({ action: 'runcommands', nodeids: [currentNode._id], type: type, cmds: Q('d2runcmd').value, runAsUser: parseInt(Q('d2cmduser').value) });
        }

        // Called when MeshCommander needs new credentials or updated credentials.
        function updateAmtCredentials(forceDialog) {
            var node = getNodeFromId(currentNode._id);
            if ((forceDialog == true) || (node.intelamt.user == null) || (node.intelamt.user == '')) {
                editDeviceAmtSettings(currentNode._id, updateAmtCredentialsEx);
            } else {
                Q('p14iframe').contentWindow.connectButtonfunctionEx();
            }
        }

        function updateAmtCredentialsEx(button, tag) {
            Q('p14iframe').contentWindow.connectButtonfunctionEx();
        }

        // Look to see if we need to update the device timeline
        function updateDeviceTimeline() {
            if ((meshserver.State != 2) || (powerTimelineNode == null) || (powerTimelineUpdate == null) || (currentNode == null) || (currentNode.mtype == 3)) return;
            if ((powerTimelineNode == powerTimelineReq) && (currentNode._id == powerTimelineNode) && (powerTimelineUpdate < Date.now())) {
                powerTimelineUpdate = null;
                meshserver.send({ action: 'powertimeline', nodeid: currentNode._id });
                meshserver.send({ action: 'lastconnect', nodeid: currentNode._id });
            }
        }

        // Draw device power bars. The bars are 766px wide.
        function drawDeviceTimeline() {
            if ((currentNode == null) || (xxcurrentView < 10) || (xxcurrentView > 19) || (currentNode.mtype == 3) || (hidePowerTimeline === 'true')) return;
            var timeline = null, now = Date.now();
            if (currentNode._id == powerTimelineNode) { timeline = powerTimeline; }

            // Calculate when the timeline starts
            var d = new Date();
            d.setHours(0, 0, 0, 0);
            d = new Date(d.getTime() - (1000 * 60 * 60 * 24 * 6));
            var timelineStart = d.getTime();

            // De-compact the timeline
            var timeline2 = [];
            if (timeline != null && timeline.length > 1) {
                timeline2.push([0, timeline[1], timeline[0]]); // Start, End, Power
                var ct = timeline[1];
                for (var i = 2; i < timeline.length; i += 2) {
                    var power = timeline[i], dt = now;
                    if (timeline.length > (i + 1)) { dt = timeline[i + 1]; }
                    timeline2.push([ct, ct + dt, power]); // Start, End, Power
                    ct = ct + dt;
                }
            }

            // Draw the timeline
            var x = '', count = 1, date = new Date();
            var totalWidth = Q('column_l').offsetWidth - (160 + 9 + 9 + 14); // Compute the total width of the power bar
            date.setHours(0, 0, 0, 0);
            for (var i = 0; i < 7; i++) {
                var datavalue = '', start = date.getTime(), end = start + (1000 * 60 * 60 * 24);
                for (var j in timeline2) {
                    var block = timeline2[j];
                    if (isTimeBlockInside(start, end, block[0], block[1]) == true) {
                        var ts = Math.max(start, block[0]);
                        var te = Math.min(Math.min(end, block[1]), now);
                        var width = Math.round(((te - ts) * totalWidth) / 86400000);
                        if (width > 0) {
                            var title = format("{0} de {1} a {2}.", powerStateStrings2[block[2]], printTime(new Date(ts)), printTime(new Date(te)));
                            datavalue += '<div class="pwState ' + powerColor(block[2]) + '" title="' + title + '" style="width:' + width + 'px;"></div>';
                        }
                    }
                }
                x += '<tr class=' + (((count % 2) == 0) ? 'altBack' : '') + '><td><div>&nbsp;' + printDate(date) + '<div></div></div></td><td><div>' + datavalue + '</div></td></tr>';
                ++count;
                date = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
            }
            // Add the language and timezone of the browser to the server so the server can localize the time correctly.
            var tz = '';
            try { tz = '&tz=' + encodeURIComponentEx(Intl.DateTimeFormat().resolvedOptions().timeZone); } catch (ex) { }
            QH('p10html2', '<table class="table table-striped align-middle text-center" cellpadding=2 cellspacing=0><thead><tr><th scope=col style=width:150px>' + "Día" + '</th><th scope=col>' + "Estado de energía de los últimos 7 días" + '<div class="float-end pe-1"><i role="button" onclick=downloadFile("devicepowerevents.ashx?id=' + currentNode._id + '&tf=' + new Date().getTimezoneOffset() + '&l=' + encodeURIComponentEx(getLang()) + tz + (urlargs.key ? ('&key=' + urlargs.key) : '') + '",null,true) title="' + "Descargar eventos de encendido" + '" class="fa-solid fa-download"></i></div></th></tr></thead><tbody>' + x + '</tbody></table>');
        }

        // Return a color for the given power state
        function powerColor(x) { if (x < powerColorTable.length) { return powerColorTable[x]; } return 'pwsYellow'; }

        // Return true if the time block is visible within the start/end period
        function isTimeBlockInside(start, end, blockStart, blockEnd) {
            if ((blockStart < start) && (blockEnd > end)) return true; // Block is wider than timespan
            if ((blockStart > start) && (blockStart < end)) return true;
            if ((blockEnd > start) && (blockEnd < end)) return true;
            return false;
        }

        function addDeviceAttribute(name, value, classNames = []) { return `<div class="row mb-1"><div class="${classNames[0] || 'col col-md-2 col-lg-2'}"><b> ${name} </b></div><div class="${classNames[1] || 'col col-md-10 col-lg-10'}"> ${value} </div></div>`; }

        function editDeviceAmtSettings(nodeid, func, arg) {
            if (xxdialogMode) return;
            var x = '', node = getNodeFromId(nodeid), buttons = 3, meshrights = GetNodeRights(node);
            if ((meshrights & 4) == 0) return;
            x += addHtmlFormFloating("Nombre de Usuario", '<input id=dp10username class="form-control" maxlength=32 autocomplete=nope placeholder="admin" onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            x += addHtmlFormFloating("Contraseña", '<input id=dp10password type=password class="form-control" autocomplete=nope maxlength=32 onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            // Only display the TLS setting if the Intel AMT manager is not running on the server. With the manager TLS is auto-detected.
            if ((features2 & 1) == 0) { x += addHtmlFormFloating("Seguridad", '<select id=dp10tls class="form-select"><option value=0>' + "Sin Seguridad TLS" + '</option><option value=1>' + "Seguridad TLS requerida" + '</option></select>'); }
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { buttons = 7; }
            setModalContent('xxAddAgent', "Editar Credenciales de Intel&reg; AMT", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => editDeviceAmtSettingsEx(buttons, { node: node, func: func, arg: arg }));
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { Q('dp10username').value = node.intelamt.user; } else { Q('dp10username').value = 'admin'; }
            if ((features2 & 1) == 0) { Q('dp10tls').value = node.intelamt.tls; }
            validateDeviceAmtSettings();
        }

        function validateDeviceAmtSettings() {
            QE('idx_dlgOkButton', passwordcheck(Q('dp10password').value));
        }

        function editDeviceAmtSettingsEx(button, tag) {
            if (button == 2) {
                // Delete button pressed, remove credentials
                meshserver.send({ action: 'changedevice', nodeid: tag.node._id, intelamt: { user: '', pass: '' } });
            } else {
                // Change Intel AMT credentials
                var amtuser = Q('dp10username').value;
                if (amtuser == '') amtuser = 'admin';
                var amtpass = Q('dp10password').value;
                if (amtpass == '') amtuser = '';
                var x = { action: 'changedevice', nodeid: tag.node._id, intelamt: { user: amtuser, pass: amtpass } };
                if ((features2 & 1) == 0) { x.intelamt.tls = parseInt(Q('dp10tls').value); }
                meshserver.send(x);
                if (tag.func) { setTimeout(function () { tag.func(null, tag.arg); }, 1000); }
            }
        }

        function p10showSendMqttMsgDialog(nodeids) {
            if (xxdialogMode) return false;
            var x = addHtmlFormFloating("Tema", '<input id=dp2topic class="form-control" maxlength=64 onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1) />');
            x += addHtmlFormFloating("Mensaje", '<textarea id=dp2msg class="form-control" maxlength=4096 style=width:100%;height:150px;resize:none onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1)></textarea>');
            setModalContent('xxAddAgent', "Enviar mensaje MQTT", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showSendMqttMsgDialogEx(3, nodeids));
            p10validateSendMqttMsgDialog();
            Q('dp2topic').focus();
            return false;
        }

        function p10validateSendMqttMsgDialog() {
            QE('idx_dlgOkButton', (Q('dp2topic').value.length > 0) && (Q('dp2msg').value.length > 0));
        }

        function p10showSendMqttMsgDialogEx(b, nodeids) {
            meshserver.send({ action: 'sendmqttmsg', nodeids: nodeids, topic: Q('dp2topic').value, msg: Q('dp2msg').value });
            uncheckAllDevices();
        }

        function p10showSendUninstallAgentDialog(nodeids) {
            if (xxdialogMode) return false;
            var x = '';
            if (nodeids.length > 1) { x = format("¿Está seguro de que desea desinstalar los {0} agentes seleccionados?", nodeids.length); } else { x = "¿Estás seguro de que desea desinstalar el agente seleccionado?"; }
            x += '<br /><br />';
            if (nodeids.length > 1) { x += "Esto no eliminará los dispositivos del servidor, pero los dispositivos ya no podrán conectarse al servidor. Se perderá todo el acceso remoto a los dispositivos. Los dispositivos deben estar conectados para que este comando funcione."; } else { x += "Esto no eliminará este dispositivo del servidor, pero el dispositivo ya no podrá conectarse al servidor. Se perderá todo el acceso remoto al dispositivo. El dispositivo debe estar conectado para que este comando funcione."; }
            x += '<br /><br /><label style=color:red><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Confirmar" + '</label>';
            setModalContent('xxAddAgent', "Desinstalar agente", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showSendUninstallAgentDialogEx(3, nodeids));

            p10validateSendUninstallAgentDialog();
            return false;
        }

        function p10validateSendUninstallAgentDialog() { QE('idx_dlgOkButton', Q('p10check').checked); }
        function p10showSendUninstallAgentDialogEx(b, nodeids) { meshserver.send({ action: 'uninstallagent', nodeids: nodeids }); uncheckAllDevices(); }

        function p10showChangeGroupDialog(nodeids) {
            if (xxdialogMode) return false;
            var targetMeshId = null;
            if (nodeids.length == 1) { try { targetMeshId = meshes[getNodeFromId(nodeids[0]).meshid]._id; } catch (ex) { } }

            // List all available alternative groups
            var y = '<select id=p10newGroup class="form-select">', count = 0, altGroups = [];
            for (var i in meshes) { var meshrights = GetMeshRights(i); if ((meshes[i]._id != targetMeshId) && (meshrights & 4)) { altGroups.push(meshes[i]); } }
            altGroups.sort(nameSort);
            for (var i in altGroups) { count++; y += '<option value=\'' + altGroups[i]._id + '\'>' + altGroups[i].name + '</option>'; }
            y += '</select>';

            if (count > 0) {
                var x = (nodeids.length == 1) ? ("Selecciona un nuevo grupo para este dispositivo" + '<br /><br />') : ("Selecciona un nuevo grupo para los dispositivos seleccionados" + '<br /><br />');
                x += addHtmlFormFloating("Nuevo Grupo de Dispositivos", y);
                setModalContent('xxAddAgent', "Cambiar Grupo de Dispositivos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showChangeGroupDialogEx(3, nodeids));
            } else {
                setModalContent('xxAddAgent', "Cambiar Grupo de Dispositivos", 'No other device group of same type exists.');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
            return false;
        }

        function p10showChangeGroupDialogEx(b, nodeids) {
            meshserver.send({ action: 'changeDeviceMesh', nodeids: nodeids, meshid: Q('p10newGroup').value });
            uncheckAllDevices();
        }

        function p10showDeleteNodeDialog(nodeid) {
            if (xxdialogMode) return false;
            var x = format("¿Estás seguro de que desea eliminar el nodo {0}?", EscapeHtml(currentNode.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Confirmar" + '</label>';
            xxdialogButtons = 3; xxdialogTag = nodeid;
            setModalContent('xxAddAgent', "Eliminar Nodo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showDeleteNodeDialogEx(0, nodeid));
            p10validateDeleteNodeDialog();
            return false;
        }

        function p10validateDeleteNodeDialog() {
            QE('idx_dlgOkButton', Q('p10check').checked);
        }

        function p10showDeleteNodeDialogEx(buttons, nodeid) {
            meshserver.send({ action: 'removedevices', nodeids: [nodeid] });
        }

        function p10WebRouter(nodeid, protocol, port, addr) {
            var relayid = null;
            var node = getNodeFromId(nodeid);
            if (node.mtype == 3) { // Setup device relay if needed
                var mesh = meshes[node.meshid];
                if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; }
            }
            var servername = serverinfo.name;
            if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
            if (webRelayDns != '') { servername = webRelayDns; }
            var url = 'https://' + servername + ':' + webRelayPort + '/control-redirect.ashx?n=' + nodeid + '&p=' + port + '&appid=' + protocol + '&c=' + authRelayCookie; // Protocol: 1 = HTTP, 2 = HTTPS
            if (addr != null) { url += '&addr=' + addr; }
            if (relayid != null) { url += '&relayid=' + relayid; }
            safeNewWindow(url, 'WebRelay');
            return false;
        }

        function p10MCRouter(nodeid, protocol, port, addr, localport) {
            var node = getNodeFromId(nodeid);
            var mesh = meshes[node.meshid];
            if ((protocol == 3) && (port == null)) { if (node.rdpport != null) { port = node.rdpport; } else { port = 3389; } } // Adjust RDP port if needed
            if (node.mtype == 3) { if (mesh && mesh.relayid) { nodeid = mesh.relayid; addr = node.host; } } // Setup device replay if needed
            meshserver.send({ action: 'getcookie', nodeid: nodeid, tcpport: port, ip: addr, tag: 'MCRouter', protocol: protocol, localport: localport, name: mesh ? mesh.name : null }); // Protocol: 0 = Custom, 1 = HTTP, 2 = HTTPS, 3 = RDP, 4 = PuTTY, 5 = WinSCP, 6 = MCRDesktop, 7 = MCRFiles
            return false;
        }

        function p10rfb(nodeid, port) {
            var node = getNodeFromId(nodeid), addr = null;
            var mesh = meshes[node.meshid];
            if (port == null) { if (node.rfbport != null) { port = node.rfbport; } else { port = 5900; } }
            if (node.mtype == 3) { if (mesh && mesh.relayid) { nodeid = mesh.relayid; addr = node.host; } } // Setup device relay if needed
            meshserver.send({ action: 'getcookie', nodeid: nodeid, tcpport: port, tcpaddr: addr, tag: 'novnc', name: mesh ? mesh.name : null });
        }

        function p10mstsc(nodeid, port) {
            var node = getNodeFromId(nodeid);
            var mesh = meshes[node.meshid];
            if (port == null) { if (node.rdpport != null) { port = node.rdpport; } else { port = 3389; } } // Adjust RDP port if needed
            meshserver.send({ action: 'getcookie', nodeid: nodeid, tcpport: port, tag: 'mstsc', name: mesh ? mesh.name : null });
        }

        function p10ssh(nodeid, port) {
            var node = getNodeFromId(nodeid);
            var mesh = meshes[node.meshid];
            if (port == null) { if (node.sshport != null) { port = node.sshport; } else { port = 22; } }
            meshserver.send({ action: 'getcookie', nodeid: nodeid, tcpport: port, tag: 'ssh', name: mesh ? mesh.name : null });
        }

        // Show current location
        var d2map = null;
        function p10showNodeLocationDialog() {
            if ((xxdialogMode != null) && (xxdialogTag == '@xxmap')) { setDialogMode(0); } else { if (xxdialogMode) return false; }
            var showLocation = function (event) {
                var markers = [], types = ['iploc', 'wifiloc', 'gpsloc', 'userloc'], boundingBox = null;

                for (var loctype in types) {
                    if (currentNode[types[loctype]] != null) {
                        var loc = currentNode[types[loctype]].split(','), lat = parseFloat(loc[0]), lon = parseFloat(loc[1]);
                        if ((lat < 90) && (lat > -90) && (lon < 180) && (lon > -180)) { // Check valid lat/lon
                            var deviceMark = new ol.Feature({ geometry: new ol.geom.Point(ol.proj.fromLonLat([lon, lat])) });
                            deviceMark.setStyle(markerStyle(currentNode, parseInt(loctype) + 1));
                            markers.push(deviceMark);

                            if (boundingBox == null) { boundingBox = [lat, lon, lat, lon, 0]; } else { if (lat < boundingBox[0]) { boundingBox[0] = lat; } if (lon < boundingBox[1]) { boundingBox[1] = lon; } if (lat > boundingBox[2]) { boundingBox[2] = lat; } if (lon > boundingBox[3]) { boundingBox[3] = lon; } }
                        }
                    }
                }

                // Setup the device mark layer
                var vectorSource = new ol.source.Vector({ features: markers });
                var vectorLayer = new ol.layer.Vector({ source: vectorSource });

                var clng = 0, clat = 0, zoom = 8;
                if (boundingBox != null) {
                    var clat = (boundingBox[0] + boundingBox[2]) / 2;
                    var clng = (boundingBox[1] + boundingBox[3]) / 2;
                    var cscale = Math.max(Math.abs(boundingBox[0] - boundingBox[2]), Math.abs(boundingBox[1] - boundingBox[3]));
                    var i = 360, zoom = -2;
                    while (i > cscale) { zoom++; i = i / 2; }
                }

                if (markers.length == 1) { zoom = 8; }

                // Setup the map
                d2map = new ol.Map({
                    target: 'd2map',
                    interactions: ol.interaction.defaults({dragPan:false, mouseWheelZoom:false}),
                    layers: [ new ol.layer.Tile({
                        source: new ol.source.OSM({
                            tileLoadFunction: function(imageTile, src) { 
                                var img = imageTile.getImage();
                                img.referrerPolicy = 'origin';
                                img.src = src;
                            }
                        })
                    }), vectorLayer ],
                    view: new ol.View({ center: ol.proj.fromLonLat([clng, clat]), zoom: zoom })
                });
                document.getElementById('xxAddAgentModal').removeEventListener('shown.bs.modal', showLocation);
            }
            document.getElementById('xxAddAgentModal').addEventListener('shown.bs.modal', showLocation);
            //var x = '<div><a href="https://www.google.com/maps/preview/@' + lat + ',' + lng + ',12z" rel="noreferrer noopener" target=_blank>Open in Google maps</a></div>';
            var x = '<div id=d2map style=width:100%;height:300px></div>';
            xxdialogTag = '@xxmap';
            setModalContent('xxAddAgent', "Ubicación del Dispositivo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            return false;
        }

        // Show network interfaces
        function p10showNodeNetInfoDialog() {
            if (xxdialogMode) return false;
            xxdialogButtons = 1; xxdialogTag = 'if' + currentNode._id;
            setModalContent('xxAddAgent', "Interfaces de Red", '<div id=d2netinfo>' + "Cargando..." + '</div>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton');

            meshserver.send({ action: 'getnetworkinfo', nodeid: currentNode._id });
            return false;
        }

        // Show MeshCentral Router dialog
        function p10showMeshRouterDialog() {
            if (xxdialogMode) return;
            var x = '<div>' + "MeshCentral Router es una herramienta de Windows para la asignación de puertos TCP. Puede, por ejemplo, RDP en un dispositivo remoto a través de este servidor." + '</div><br />'; x += '<div class="row mb-1"><div class="col-md-4"><b>Win32 Executable</b></div><div class="col-md-8"><a style="cursor:pointer" onclick="downloadFile(\'meshagents?meshaction=winrouter' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '\',null,true)" class="link-primary">MeshCentralRouter.exe</a></div></div>';
            x += '<div class="row mb-1"><div class="col-md-4"><b>MacOS Installer</b></div><div class="col-md-8"><a style="cursor:pointer" onclick="downloadFile(\'meshagents?meshaction=macrouter' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '\',null,true)" class="link-primary">MeshCentralRouter.dmg</a></div></div>';
            var servername = serverinfo.name;
            if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
            var domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1);
            var portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port);
            var url = 'mcrouter://' + servername + portStr + domainUrl + 'control.ashx?c=' + authCookie + '&t=' + serverinfo.tlshash + '&l={{{lang}}}' + (urlargs.key ? ('&key=' + urlargs.key) : '');
            //x += addHtmlValue("Launch", '<a style=cursor:pointer target="mcrouterframe" rel="noreferrer noopener" download href="' + url + '">Start MeshCentral Router</a>');
            //x += '<br /><div style=width:100%><a style=cursor:pointer target="mcrouterframe" rel="noreferrer noopener" download href="' + url + '">' + "Start MeshCentral Router" + '</a>' + ", for this link to work you must download MeshCentral Router run it and click the install button." + '</div>';
            x += '<br /><div class="alert alert-info">' + "Ejecute MeshCentral Router y haz clic en \"instalar\" para que se pueda iniciar desde el navegador." + '</div>';
            x += '<br /><a style=cursor:pointer target="mcrouterframe" rel="noreferrer noopener" href="' + url + '"><input type=button style=width:100%;cursor:pointer value="' + "Ejecutar MeshCentral Router" + '" class="btn btn-primary btn-block" /></a>';
            x += '<iframe class="d-none" name="mcrouterframe"></iframe>';
            setModalContent('xxAddAgent', "Router de MeshCentral ", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p20showAddMeshUserDialogEx(1, 'fileDownload');
            });
        }

        // Request MQTT login credentials
        function p10showMqttLoginDialog(nodeid) { meshserver.send({ action: 'getmqttlogin', nodeid: nodeid }); }

        // Open XTerm
        function p10openxterm(e, nodeid) {
            haltEvent(e);
            var url = '/xterm?nodeid=' + encodeURIComponentEx(nodeid) + '&auto=1';
            var node = getNodeFromId(nodeid);
            if (node == null) return;
            if ([1, 2, 3, 4, 21, 22].indexOf(node.agent.id) >= 0) { url += '&os=win'; } else { url += '&os=linux'; }
            if (urlargs.key) { url += '&key=' + urlargs.key; }
            safeNewWindow(url, 'xterm:' + nodeid);
            return false;
        }

        // Show MeshCmd dialog
        function p10showMeshCmdDialog(mode, nodeid) {
            if (xxdialogMode) return;
            var y = '<select id=aginsSelect onclick=meshCmdOsClick() class=form-select>';
            y += '<option value=4>' + "Windows x86 (64bit)" + '</option>';
            y += '<option value=3>' + "Windows x86 (32bit)" + '</option>';
            y += '<option value=43>' + "Windows ARM (64bit)" + '</option>';
            y += '<option value=6>' + "Linux x86 (64bit)" + '</option>';
            y += '<option value=5>' + "Linux x86 (32bit)" + '</option>';
            y += '<option value=16>' + "macOS x86 (64bit)" + '</option>';
            y += '<option value=29>' + "macOS ARM (64bit)" + '</option>';
            y += '<option value=25>' + "Linux ARM, Raspberry Pi (32bit)" + '</option>';
            y += '<option value=26>' + "Linux ARM, Raspberry Pi (64bit)" + '</option>';
            y += '</select>';

            var x = '';
            if (mode == 0) { x += '<div>' + "MeshCmd es una herramienta de línea de comandos que realiza muchas operaciones diferentes. El archivo de acción se puede descargar y editar opcionalmente para proporcionar información del servidor y credenciales." + '<br /><br />'; }
            if (mode == 1) { x += '<div>' + "Descarga \"meshcmd\" con un archivo de acción para enrutar el tráfico a través de este servidor a este dispositivo. Asegúrate de editar meshaction.txt y agregar la contraseña de su cuenta o realiza los cambios necesarios." + '<br /><br />'; }
            x += addHtmlFormFloating("Sistema Operativo", y);
            x += addHtmlValue("MeshCmd", '<a id=meshcmddownloadid onclick=downloadFile("meshagents?meshcmd=3' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary></a>');
            if (mode == 0) { x += addHtmlValue("Archivo de acción", '<a onclick=downloadFile("meshagents?meshaction=generic' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary>' + "MeshAction (.txt)" + '</a>'); }
            if (mode == 1) { x += addHtmlValue("Archivo de acción", '<a onclick=downloadFile("meshagents?meshaction=route&nodeid=' + nodeid + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary>' + "MeshAction (.txt)" + '</a>'); }
            x += '</div>';
            xxdialogButtons = 9; xxdialogMode = 'fileDownload';
            setModalContent('xxAddAgent', "Descargar MeshCmd", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            meshCmdOsClick();
        }

        function meshCmdOsClick() {
            var os = Q('aginsSelect').value, osn = '', osurl = '';
            //Q('meshcmddownloadid').href = 'meshagents?meshcmd=' + os + (urlargs.key?('&key=' + urlargs.key):'');
            if (os == 3) { osn = "MeshCmd (ejecutable Win x86-32)"; }
            if (os == 4) { osn = "MeshCmd (ejecutable Win x86-64)"; }
            if (os == 43) { osn = "MeshCmd (ejecutable Win ARM-64)"; }
            if (os == 5) { osn = "MeshCmd (Linux x86, 32bit)"; }
            if (os == 6) { osn = "MeshCmd (Linux x86, 64bit)"; }
            if (os == 16) { osn = "MeshCmd (macOS, x86-64bit)"; }
            if (os == 29) { osn = "MeshCmd (macOS, ARM-64bit)"; }
            if (os == 25) { osn = "MeshCmd (Linux ARM, 32bit)"; }
            if (os == 26) { osn = "MeshCmd (Linux ARM, 64bit)"; }
            QH('meshcmddownloadid', osn);
            Q('meshcmddownloadid').onclick = function () { downloadFile('meshagents?meshcmd=' + os + (urlargs.key ? ('&key=' + urlargs.key) : '')); }
        }

        function p10showiconselector() {
            if (xxdialogMode) return;
            if ((GetNodeRights(currentNode) & 4) == 0) return;

            var x = '<div style=text-align:center><br />';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i1 onclick=p10setIcon(1) onkeypress="if (event.key==\'Enter\') p10setIcon(1)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i2 onclick=p10setIcon(2) onkeypress="if (event.key==\'Enter\') p10setIcon(2)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i3 onclick=p10setIcon(3) onkeypress="if (event.key==\'Enter\') p10setIcon(3)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i4 onclick=p10setIcon(4) onkeypress="if (event.key==\'Enter\') p10setIcon(4)"></div>';
            x += '<br />';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i5 onclick=p10setIcon(5) onkeypress="if (event.key==\'Enter\') p10setIcon(5)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i6 onclick=p10setIcon(6) onkeypress="if (event.key==\'Enter\') p10setIcon(6)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i7 onclick=p10setIcon(7) onkeypress="if (event.key==\'Enter\') p10setIcon(7)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i8 onclick=p10setIcon(8) onkeypress="if (event.key==\'Enter\') p10setIcon(8)"></div>';
            x += '<br /><br /></div>';
            setModalContent('xxAddAgent', "Selección de Icono", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            QV('id_dialogclose', true);
        }

        function p10setIcon(icon) {
            setDialogMode(0);
            meshserver.send({ action: 'changedevice', nodeid: currentNode._id, icon: icon });
        }

        function showClearSshDialog() {
            setModalContent('xxAddAgent', "Editar Dispositivo", 'Clear SSH credentials?');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showClearSshDialogEx(3));
        }
        function showClearSshDialogEx(button, mode) { meshserver.send({ action: 'changedevice', nodeid: currentNode._id, ssh: 0 }); }
        function showClearRdpDialog() {
            setModalContent('xxAddAgent', "Editar Dispositivo", 'Clear RDP credentials?');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showClearRdpDialogEx(3));
        }
        function showClearRdpDialogEx(button, mode) { meshserver.send({ action: 'changedevice', nodeid: currentNode._id, rdp: 0 }); }

        var showEditNodeValueDialog_modes = ["Nombre del Dispositivo", "Nombre de Host", "Descripción", "Etiquetas"];
        var showEditNodeValueDialog_modes2 = ['name', 'host', 'desc', 'tags'];
        var showEditNodeValueDialog_modes3 = ['', '', '', "Etiqueta1, Etiqueta2, Etiqueta3"];
        var showEditNodeValueDialog_modes4 = [64, 64, 64, 4096];
        function showEditNodeValueDialog(mode) {
            if (xxdialogMode) return;
            var x = '';
            var y = '';
            var v = currentNode[showEditNodeValueDialog_modes2[mode]];
            if (v == null) v = '';
            if (mode == 3) {
                // Get a list of all possible device tags
                x = '<select id=dp10devicevalue multiple maxlength=' + showEditNodeValueDialog_modes4[mode] + ' class=form-control>';
                var allTags = [];
                for (var i in nodes) { if (nodes[i].tags) { for (var j in nodes[i].tags) { if (allTags.indexOf(nodes[i].tags[j]) == -1) { allTags.push(nodes[i].tags[j]); } } } }
                if (allTags.length > 0) {
                    allTags.sort();
                    for (var i in allTags) {
                        y += Array.isArray(v) && v.indexOf(allTags[i]) !== -1 ? '<option selected=selected>' + EscapeHtml(allTags[i]) + '</option>' : '<option>' + EscapeHtml(allTags[i]) + '</option>';
                    }
                }
                x += y + '</select>'
                setModalContent('xxAddAgent', "Edit Tags", x);
                $('#dp10devicevalue').select2({
                    theme: 'bootstrap-5',
                    width: $( this ).data( 'width' ) ? $( this ).data( 'width' ) : $( this ).hasClass( 'w-100' ) ? '100%' : 'style',
                    placeholder: showEditNodeValueDialog_modes3[mode],
                    closeOnSelect: false,
                    allowClear: true,
                    tokenSeparators: [','],
                    tags: true
                });
            } else {
                x = addHtmlFormFloating(showEditNodeValueDialog_modes[mode], '<input id=dp10devicevalue class="form-control" maxlength=' + showEditNodeValueDialog_modes4[mode] + ' placeholder="' + showEditNodeValueDialog_modes3[mode] + '" onchange=p10editdevicevalueValidate(' + mode + ',event) onkeyup=p10editdevicevalueValidate(' + mode + ',event) class=form-control/>');
                setModalContent('xxAddAgent', "Editar Dispositivo", x);
                if (Array.isArray(v)) { v = v.join(', '); }
                Q('dp10devicevalue').value = v;
            }
            p10editdevicevalueValidate();
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { showEditNodeValueDialogEx(3, mode); });
            Q('dp10devicevalue').focus();
        }

        function showEditNodeValueDialogEx(button, mode) {
            var tags = '';
            var x = { action: 'changedevice', nodeid: currentNode._id };
            if (mode == 3) {
                var tt = $('#dp10devicevalue').select2('data');
                for (var i in tt) { tags += tt[i]['text'].trim() + ', '; }
                x[showEditNodeValueDialog_modes2[mode]] = decodeURIComponent(tags.slice(0,-2));
            } else {
                x[showEditNodeValueDialog_modes2[mode]] = Q('dp10devicevalue').value;
            }
            meshserver.send(x);
        }

        function p10editdevicevalueValidate(mode, e) {
            var x = ((mode > 1) || (Q('dp10devicevalue').value.length > 0));
            QE('idx_dlgOkButton', x);
            if ((e != null) && (x == true) && (e.keyCode == 13)) { dialogclose(1); }
        }

        //
        // DESKTOP
        //

        var desktopNode;
        function setupDesktop() {
            // Setup the remote desktop
            if ((desktopNode != currentNode) && (desktop != null)) {
                if (desktopNode._id != currentNode._id) {
                    desktop.Stop(); desktopNode = null; desktop = null;
                }
            }

            // If the device desktop is already connected in multi-desktop, use that.
            if ((desktopNode != currentNode) || (desktop == null)) {
                var xdesk = multiDesktop[currentNode._id];
                if (xdesk != null) {
                    // This device already has a canvas, use it.
                    QH('DeskParent', '');
                    var c = xdesk.m.CanvasId;
                    c.setAttribute('id', 'Desk');
                    c.setAttribute('onmousedown', 'dmousedown(event)');
                    c.setAttribute('onmouseup', 'dmouseup(event)');
                    c.setAttribute('onmousemove', 'dmousemove(event)');
                    c.removeAttribute('onclick');
                    Q('DeskParent').appendChild(c);
                    desktop = xdesk;
                    if (desktop.m.SendCompressionLevel) { desktop.m.SendCompressionLevel(desktopsettings.agentencoding, desktopsettings.quality, desktopsettings.scaling, desktopsettings.framerate); }
                    desktop.onStateChanged = onDesktopStateChange;
                    desktop.onMetadataChange = function (metadata) { updateMetadata(desktop, 'deskmetadata'); }
                    if ((features2 & 0x2000) != 0) desktop.m.stopInput = true;
                    if (desktop && desktop.m.mouseCursorActive) { desktop.m.mouseCursorActive(true); }
                    QV('DeskInputLockedButton', desktop.m.RemoteInputLock == true);
                    QV('DeskInputUnLockedButton', desktop.m.RemoteInputLock == false);
                    desktop.m.onRemoteInputLockChanged = function (obj, state) { QV('DeskInputLockedButton', state); QV('DeskInputUnLockedButton', !state); }
                    desktop.m.onKeyboardStateChanged = function (obj, state) {
                        QS('p11numlock').display = ((state & 1) ? 'inline-block' : 'none');
                        QS('p11scrolllock').display = ((state & 2) ? 'inline-block' : 'none');
                        QS('p11capslock').display = ((state & 4) ? 'inline-block' : 'none');
                    }
                    desktopNode = currentNode;
                    onDesktopStateChange(desktop, desktop.State);
                    delete multiDesktop[currentNode._id];
                } else {
                    // Device is not already connected, just setup a blank canvas
                    if (desktop == null) { QH('DeskParent', '<canvas id=Desk oncontextmenu="return false" onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event) ontouchstart=dtouchstart(event) ontouchmove=dtouchmove(event) ontouchend=dtouchend(event)></canvas>'); }
                    desktopNode = currentNode;
                }
                // Setup the mouse wheel
                Q('Desk').addEventListener('DOMMouseScroll', function (e) { return dmousewheel(e); });
                Q('Desk').addEventListener('mousewheel', function (e) { return dmousewheel(e); });
            }
            desktopNode = currentNode;
            if (desktop) { desktop.m.onDisplayinfo = deskDisplayInfo; deskDisplayInfo(desktop.m, desktop.m.displays, desktop.m.selectedDisplay); }
            updateDesktopButtons();
            deskAdjust();

            updateMetadata(desktop, 'deskmetadata');
        }

        // Show and enable the right buttons
        function updateDesktopButtons() {
            var deskState = 0;
            if (desktop != null) { deskState = desktop.State; }
            var rights = GetNodeRights(currentNode);
            var mtype = (currentNode.agent == 1) ? 1 : 2;

            // Show the right buttons
            QV('disconnectbutton1span', (deskState != 0));
            QV('connectbutton1span', (deskState == 0) && ((rights & 8) || (rights & 256)) && (currentNode.agent != null) && (currentNode.agent.caps & 1));
            QV('connectbutton1rspan', ((features & 0x40000000) == 0) && (deskState == 0) && (rights & 8) && (currentNode.agent != null) && ((currentNode.agent.id == 3) || (currentNode.agent.id == 4)));
            if (mtype == 1) {
                QV('connectbutton1hspan',
                    (deskState == 0) &&
                    (rights & 8) &&
                    (
                        ((currentNode.intelamt != null) &&
                            (currentNode.intelamt.state == 2))
                    )
                );
            } else {
                QV('connectbutton1hspan',
                    (deskState == 0) &&
                    (rights & 8) &&
                    (
                        ((currentNode.intelamt != null) &&
                            (currentNode.intelamt.state == 2) &&
                            (currentNode.intelamt.ver != null) &&
                            ((currentNode.intelamt.sku == null) ||
                                ((typeof currentNode.intelamt.sku == 'number') &&
                                    ((currentNode.intelamt.sku & 8) != 0))))
                    )
                );
            }
            // Show the right settings
            QV('td7amtkvm', ((currentNode.intelamt != null) && ((typeof currentNode.intelamt.sku != 'number') || ((currentNode.intelamt.sku & 16) == 0)) && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
            QV('td7meshkvm', (webRtcDesktop) || ((currentNode.agent != null) && (currentNode.agent.caps & 1) && ((deskState == 0) || (desktop.contype == 1))));
            QV('td7rdpkvm', ((currentNode.agent != null) && ((currentNode.agent.id == 3) || (currentNode.agent.id == 4)) && ((deskState == 0) || (desktop.contype == 4))));

            // Enable buttons
            var inputAllowed = ((features2 & 0x2000) == 0) && ((currentNode.agent == null) || (currentNode.agent.id != 14)) && ((rights == 0xFFFFFFFF) || (((rights & 8) != 0) && ((rights & 256) == 0) && ((rights & 4096) == 0)));
            var online = ((currentNode.conn & 1) != 0); // If Agent (1) connected, enable remote desktop
            QE('connectbutton1', online); QE('connectbutton1d', online); // Desktop Connect button and dropdown
            QE('connectbutton1r', online || (currentNode.mtype == 3));  QE('connectbutton1rd', online || (currentNode.mtype == 3)); // Desktop RDP Connect button and dropdown
            if (currentNode.rdpport && currentNode.rdpport != 3389) {
                QH('desktopCustomUpperRight', '<a style="cursor:pointer;line-height:22px" onclick="cmaltportaction(1,event)">' + format("RDP Port {0}", (currentNode.rdpport ? currentNode.rdpport : 3389)) + '</a>');
            } else {
                QH('desktopCustomUpperRight', '');
            }
            var hwonline = ((currentNode.conn & 6) != 0); // If CIRA (2) or AMT (4) connected, enable hardware terminal
            QE('connectbutton1h', hwonline);
            QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (deskState != 0) && (desktopsettings.showfocus));
            QE('DeskClip', deskState == 3);
            QE('DeskSoftKbdBtn', deskState == 3);
            QV('DeskClip', (inputAllowed) && (currentNode.agent) && ((features2 & 0x1800) != 0x1800) && (currentNode.agent.id != 11) && (currentNode.agent.id != 16) && ((desktop == null) || (desktop.contype != 2)) && ((desktopsettings.autoclipboard != true) || (navigator.clipboard == null) || (navigator.clipboard.readText == null))); // Clipboard not supported on macOS
            QE('DeskESC', (deskState == 3) && (desktop.contype != 4));
            QV('DeskESC', browserfullscreen && inputAllowed);
            QE('DeskType', deskState == 3);
            QV('DeskType', inputAllowed);
            QE('DeskWD', deskState == 3);
            QV('DeskWD', inputAllowed);
            QV('deskkeys', inputAllowed);
            QE('deskkeys', desktop != null);
            QV('DeskTimer', deskState == 3);
            QV('DeskLatency', deskState == 3);
            QS('DeskLatency').display = (deskState == 3 ? 'inline-block' : 'none');

            // Enable browser clipboard read if supported
            var autoclipboard = ((desktop) && (desktop.contype == 4)) ? desktopsettings.rdpautoclipboard : desktopsettings.autoclipboard;
            QV('DeskClipboardOutButton', online && inputAllowed && (desktop != null) && ((features2 & 0x1000) == 0) && (navigator.clipboard != null) && (navigator.clipboard.readText != null) && ((autoclipboard != true) || (navigator.clipboard == null) || (navigator.clipboard.readText == null)));
            QV('d7deskAutoClipboardLabel', (navigator.clipboard.readText != null) && ((features2 & 0x1000) == 0));
            QV('DeskClipboardInButton', online && inputAllowed && (desktop != null) && ((features2 & 0x0800) == 0) && (navigator.clipboard != null) && (navigator.clipboard.writeText != null) && ((autoclipboard != true) || (navigator.clipboard == null) || (navigator.clipboard.readText == null)));
            if (deskState != 3) {
                QV('DeskInputLockedButton', false);
                QV('DeskInputUnLockedButton', false);
                QV('p11numlock', false);
                QV('p11scrolllock', false);
                QV('p11capslock', false);
            }

            // Display this only if we have Chat & Notify permissions
            QV('DeskRunButton', ((rights & 131072) != 0) && online);
            QV('DeskSaveImageButton', (deskState == 3) && (Q('Desk')['toBlob'] != null) && ((features2 & 0x400) == 0));
            QV('DeskRecordButton', (deskState == 3) && (Q('Desk')['toBlob'] != null) && (desktop.m.StartRecording != null) && ((features2 & 0x400) == 0));
            QV('DeskChatButton', ((rights & 16384) != 0) && (browserfullscreen == false) && (currentNode.agent) && online);
            QV('DeskNotifyButton', ((rights & 16384) != 0) && (browserfullscreen == false) && (currentNode.agent) && online);
            QV('DeskLockButton', ((rights & 16384) != 0) && (browserfullscreen == false) && (currentNode.agent) && (currentNode.agent.id < 5) && (inputAllowed) && (deskState == 3));
            QV('DeskToolsButton', (currentNode.agent) && online);
            QE('DeskToolsButton', inputAllowed);
            QV('DeskOpenWebButton', (browserfullscreen == false) && (inputAllowed) && (currentNode.agent) && online);
            QV('DeskBackgroundButton', inputAllowed && (deskState == 3) && (desktop.contype == 1) && (currentNode.agent) && (currentNode.agent.id != 11) && (currentNode.agent.id != 16) && online);
            QV('DeskControlSpan', inputAllowed)
            QV('DeskRefreshButton', (deskState == 3) && (desktop.contype == 1))
            if (rights & 8) { Q('DeskControl').checked = (getstore('DeskControl', 1) == 1); } else { Q('DeskControl').checked = false; }
            QS('DeskControlSpan').color = Q('DeskControl').checked ? null : 'red';

            if (online == false) QV('DeskTools', false);
        }

        // Debug
        var autoConnectDesktopTimer = null;
        function autoConnectDesktop(e) { if (autoConnectDesktopTimer == null) { autoConnectDesktopTimer = setInterval(function () { connectDesktop(null, 1) }, 1000); } else { clearInterval(autoConnectDesktopTimer); autoConnectDesktopTimer = null; } }

        // Used to translate incoming agent console messages
        var agentConsoleMessages = ['', "Esperando a que el usuario otorgue acceso...", "Denegado", "Error al iniciar sesión de terminal remota, {0} ({1})", "Timeout", "Se recibieron datos de red inválidos", "No se puede capturar la pantalla", "Negociación de protocolo fallida ({0})", "NLA no compatible", "SSL requerido por el servidor", "SSL no permitido por el servidor", "Certificado SSL no en el servidor", "Banderas inconsistentes", "Híbrido requerido por el servidor", "SSL con autenticación de usuario requerida por el servidor"];
        function formatAgentConsoleMessage(msg, msgid, msgargs) {
            var r;
            if (msgargs == null) { msgargs = []; }
            while (msgargs.length < 3) { msgargs.push(''); } // We need to call the format function in a way that works with older browsers and minifier, can't use apply() or ...
            if (msgid && (msgid < agentConsoleMessages.length)) { r = EscapeHtml(format(agentConsoleMessages[msgid], (msgargs[0]), (msgargs[1]), (msgargs[2]))); } else { r = EscapeHtml(msg); }
            return r.split('\n').join('<br />') + '<br /><br />';
        }

        function connectDesktop(e, contype, tsid, consent) {
            if (xxdialogMode) return;
            if ((e != null) && (e.shiftKey != false) && (contype == 3)) { contype = 1; } // If the shift key is not pressed, don't try to ask for session list.
            QV('p11DeskSessionSelector', false);
            p11clearConsoleMsg();
            if (desktop == null) {
                desktopNode = currentNode;
                if (contype == 2) {
                    // Setup the Intel AMT remote desktop
                    if ((desktopNode.intelamt.user == null) || (desktopNode.intelamt.user == '')) { editDeviceAmtSettings(desktopNode._id, connectDesktop, 2); return; }
                    desktop = CreateAmtRedirect(CreateAmtRemoteDesktop('Desk'), authCookie);
                    desktop.debugmode = debugmode;
                    desktop.onStateChanged = onDesktopStateChange;
                    desktop.m.bpp = (desktopsettings.encoding == 1 || desktopsettings.encoding == 3) ? 1 : 2;
                    desktop.m.useZRLE = (desktopsettings.encoding < 3);
                    desktop.m.localKeyMap = desktopsettings.localkeymap;
                    desktop.m.ReverseMouseWheel = desktopsettings.kvmrmw;
                    desktop.m.showmouse = desktopsettings.showmouse;
                    desktop.m.onScreenSizeChange = deskAdjust;
                    desktop.m.onKvmData = function (x) {
                        //console.log('onKvmData (' + x.length + '): ' + x);
                        // Send the presense probe only once if needed.
                        if (x.length == 0) { if (!desktop.m._sentPresence) { desktop.m._sentPresence = true; desktop.m.sendKvmData(JSON.stringify({ action: 'present', ver: 1 })); } return; }
                        var data = null;
                        try { data = JSON.parse(x); } catch (e) { }
                        if ((data != null) && (data.action != null)) {
                            if (data.action == 'restart') {
                                // Clear WebRTC channel
                                webRtcDesktopReset();
                                desktop.m.sendKvmData(JSON.stringify({ action: 'present', ver: 1 }));
                            } else if ((data.action == 'present') && (webRtcDesktop == null)) {
                                // Setup WebRTC channel
                                webRtcDesktop = { platform: data.platform };
                                var configuration = null; //{ "iceServers": [ { 'urls': 'stun:stun.cloudflare.com:3478' }, { 'urls': 'stun:stun.l.google.com:19302' } ] };
                                if (typeof RTCPeerConnection !== 'undefined') { webRtcDesktop.webrtc = new RTCPeerConnection(configuration); }
                                else if (typeof webkitRTCPeerConnection !== 'undefined') { webRtcDesktop.webrtc = new webkitRTCPeerConnection(configuration); }

                                webRtcDesktop.webchannel = webRtcDesktop.webrtc.createDataChannel("Canal de Datos", {}); // { ordered: false, maxRetransmits: 2 }
                                webRtcDesktop.webchannel.onopen = function () {
                                    // Switch to software KVM
                                    //if (urlvars && urlvars['kvmdatatrace']) { console.log('WebRTC Data Channel Open'); }
                                    console.log('WebRTC Data Channel Open');
                                    Q('deskstatus').textContent = StatusStrs[desktop.State] + ", Soft-KVM";
                                    desktop.m.hold(true);
                                    webRtcDesktop.webRtcActive = true;
                                    webRtcDesktop.softdesktop = CreateKvmDataChannel(webRtcDesktop.webchannel, CreateAgentRemoteDesktop('Desk', Q('id_mainarea')), desktop.m);
                                    webRtcDesktop.softdesktop.m.setRotation(desktop.m.rotation);
                                    webRtcDesktop.softdesktop.m.onScreenSizeChange = deskAdjust;
                                    webRtcDesktop.softdesktop.m.ImageType = webpSupport ? 4 : 1; // Send 4 if WebP is supported, otherwise send 1 for JPEG.
                                    if (desktopsettings.quality) { webRtcDesktop.softdesktop.m.CompressionLevel = desktopsettings.quality; } // Number from 1 to 100. 50 or less is best.
                                    if (desktopsettings.scaling) { webRtcDesktop.softdesktop.m.ScalingLevel = desktopsettings.scaling; }
                                    webRtcDesktop.softdesktop.Start();

                                    // Check if we can get remote file access
                                    // ###BEGIN###{DesktopInbandFiles}
                                    /*
                                    QV('go24', true); // Files
                                    gdownloadFile = null;
                                    p24files = webRtcDesktop.softdesktop;
                                    p24targetpath = '';
                                    webRtcDesktop.softdesktop.onControlMsg = onFilesControlData;
                                    webRtcDesktop.softdesktop.sendCtrlMsg(JSON.stringify({ action: 'ls', reqid: 1, path: '' })); // Ask for the root folder
                                    */
                                    // ###END###{DesktopInbandFiles}
                                }
                                webRtcDesktop.webchannel.onclose = function (event) {
                                    //if (urlvars['kvmdatatrace']) { console.log('WebRTC Data Channel Closed'); }
                                    console.log('WebRTC Data Channel Closed');
                                    webRtcDesktopReset();
                                }
                                webRtcDesktop.webrtc.onicecandidate = function (e) {
                                    if (e.candidate == null) {
                                        desktop.m.sendKvmData(JSON.stringify({ action: 'offer', ver: 1, sdp: webRtcDesktop.webrtcoffer.sdp }));
                                    } else {
                                        webRtcDesktop.webrtcoffer.sdp += ('a=' + e.candidate.candidate + '\r\n'); // New candidate, add it to the SDP
                                    }
                                }
                                webRtcDesktop.webrtc.oniceconnectionstatechange = function () {
                                    if ((webRtcDesktop != null) && (webRtcDesktop.webrtc != null) && ((webRtcDesktop.webrtc.iceConnectionState == 'disconnected') || (webRtcDesktop.webrtc.iceConnectionState == 'failed'))) { /*console.log('WebRTC ICE Failed');*/ webRtcDesktopReset(); }
                                }
                                webRtcDesktop.webrtc.createOffer(function (offer) {
                                    // Got the offer
                                    webRtcDesktop.webrtcoffer = offer;
                                    webRtcDesktop.webrtc.setLocalDescription(offer, function () { }, webRtcDesktopReset);
                                }, webRtcDesktopReset, { mandatory: { OfferToReceiveAudio: false, OfferToReceiveVideo: false } });
                            } else if ((data.action == 'answer') && (webRtcDesktop != null)) {
                                // Complete the WebRTC channel
                                webRtcDesktop.webrtc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: data.sdp }), function () { }, webRtcDesktopReset);
                            }
                        }
                    };
                    // Use TLS if TLS is set
                    if (desktopNode.conn == 4 && desktopNode.intelamt != null && desktopNode.intelamt.tls == 1) {
                        desktop.Start(desktopNode._id, 16995, '*', '*', 1);
                    } else {
                        desktop.Start(desktopNode._id, 16994, '*', '*', 0);
                    }
                    desktop.contype = 2;
                } else if ((contype == null) || (contype == 1) || ((contype == 3) && ((currentNode.agent.id > 4) && ((debugmode == null))))) {
                    // Setup the Mesh Agent remote desktop
                    desktop = CreateAgentRedirect(meshserver, CreateAgentRemoteDesktop('Desk'), serverPublicNamePort, authCookie, authRelayCookie, domainUrl);
                    desktop.m.UseExtendedKeyFlag = (desktopNode.agent.id < 5); // Only use extended keys on Windows agents for now
                    desktop.m.mouseCursorActive(xxcurrentView == 11);
                    desktop.debugmode = debugmode;
                    desktop.m.debugmode = debugmode;
                    desktop.attemptWebRTC = attemptWebRTC;
                    desktop.webrtcconfig = webrtcconfiguration;
                    desktop.options = {};
                    if (tsid != null) { desktop.options.tsid = tsid; }
                    if (consent != null) { desktop.options.consent = consent; }
                    if (desktopsettings.autolock == true) { desktop.options.autolock = true; }
                    desktop.onStateChanged = onDesktopStateChange;
                    if ((features2 & 0x2000) != 0) desktop.m.stopInput = true;
                    desktop.m.onRemoteInputLockChanged = function (obj, state) { QV('DeskInputLockedButton', state); QV('DeskInputUnLockedButton', !state); }
                    desktop.m.onKeyboardStateChanged = function (obj, state) {
                        QS('p11numlock').display = ((state & 1) ? 'inline-block' : 'none');
                        QS('p11scrolllock').display = ((state & 2) ? 'inline-block' : 'none');
                        QS('p11capslock').display = ((state & 4) ? 'inline-block' : 'none');
                    }
                    desktop.onConsoleMessageChange = function () {
                        if (desktop.consoleMessage) {
                            Q('p11DeskConsoleMsg').innerHTML += formatAgentConsoleMessage(desktop.consoleMessage, desktop.consoleMessageId, desktop.consoleMessageArgs);
                            QV('p11DeskConsoleMsg', true);
                            if (p11DeskConsoleMsgTimer != null) { clearTimeout(p11DeskConsoleMsgTimer); }
                            if (desktop.consoleMessageTimeout) { p11DeskConsoleMsgTimer = setTimeout(p11clearConsoleMsg, desktop.consoleMessageTimeout * 1000); }
                        } else {
                            p11clearConsoleMsg();
                        }
                    }
                    desktop.onMetadataChange = function (metadata) { updateMetadata(desktop, 'deskmetadata'); }
                    desktop.m.ImageType = desktopsettings.agentencoding; // Send 4 if WebP is supported, otherwise send 1 for JPEG.
                    desktop.m.CompressionLevel = desktopsettings.quality; // Number from 1 to 100. 50 or less is best.
                    desktop.m.ScalingLevel = desktopsettings.scaling;
                    if (desktopsettings.framerate) { desktop.m.FrameRateTimer = desktopsettings.framerate; }
                    if (desktopsettings.swapmouse) { desktop.m.SwapMouse = desktopsettings.swapmouse; }
                    if (desktopsettings.rmw) { desktop.m.ReverseMouseWheel = desktopsettings.rmw; }
                    if (desktopsettings.remotekeymap == true) { desktop.m.remoteKeyMap = desktopsettings.remotekeymap; }
                    //desktop.m.onDisplayinfo = deskDisplayInfo;
                    desktop.m.onScreenSizeChange = deskAdjust;
                    if (document.body.classList.contains('is-mobile')) {
                        desktop.m.GetPositionOfControl = function(canvas) {
                            var r = canvas.getBoundingClientRect();
                            return [r.left + window.scrollX, r.top + window.scrollY];
                        };
                        var _origGrab = desktop.m.GrabMouseInput;
                        var _origUngrab = desktop.m.UnGrabMouseInput;
                        desktop.m.GrabMouseInput = function() {
                            _origGrab.call(desktop.m);
                            var c = desktop.m.CanvasId;
                            c.ontouchstart = null;
                            c.ontouchmove = null;
                            c.ontouchend = null;
                            c.addEventListener('touchstart', dtouchstart, { passive: false });
                            c.addEventListener('touchmove', dtouchmove, { passive: false });
                            c.addEventListener('touchend', dtouchend, { passive: false });
                        };
                        desktop.m.UnGrabMouseInput = function() {
                            var c = desktop.m.CanvasId;
                            c.removeEventListener('touchstart', dtouchstart);
                            c.removeEventListener('touchmove', dtouchmove);
                            c.removeEventListener('touchend', dtouchend);
                            _origUngrab.call(desktop.m);
                        };
                        // Override SendMouseMsg to fix the bounds check for rotated canvas.
                        // After setRotation(1), canvas.width=1080 & canvas.height=1920 (swapped).
                        // The original check is X<=canvas.width (1080), but our rotation-corrected
                        // X (= remote.x) can be up to 1920 — clicks in the lower half of the
                        // rotated view are silently dropped. We replace the check with the correct
                        // unrotated remote dimensions.
                        (function() {
                            var dm = desktop.m, _origSMM = dm.SendMouseMsg;
                            dm.SendMouseMsg = function(Action, event) {
                                var rot = this.rotation;
                                if (!rot) return _origSMM.call(this, Action, event);
                                if (Action == null || !this.Canvas) return;
                                var cw = this.Canvas.canvas.width, ch = this.Canvas.canvas.height;
                                var ScaleW = cw / this.CanvasId.clientWidth;
                                var ScaleH = ch / this.CanvasId.clientHeight;
                                var off = this.GetPositionOfControl(this.Canvas.canvas);
                                var X = (event.pageX - off[0]) * ScaleW;
                                var Y = (event.pageY - off[1]) * ScaleH;
                                if (event.addx) X += event.addx;
                                if (event.addy) Y += event.addy;
                                // Correct max: for rot=1,3 remote.x<=ch and remote.y<=cw (unrotated dims)
                                var maxX = (rot===1||rot===3) ? ch : cw;
                                var maxY = (rot===1||rot===3) ? cw : ch;
                                if (X<0||X>maxX||Y<0||Y>maxY) return;
                                var Button = 0;
                                if (Action===this.KeyAction.UP||Action===this.KeyAction.DOWN) {
                                    if (event.which) { Button=(event.which===1)?this.MouseButton.LEFT:((event.which===2)?this.MouseButton.MIDDLE:this.MouseButton.RIGHT); }
                                    else if (typeof event.button==='number') { Button=(event.button===0)?this.MouseButton.LEFT:((event.button===1)?this.MouseButton.MIDDLE:this.MouseButton.RIGHT); }
                                }
                                if (this.SwapMouse) { if(Button===this.MouseButton.LEFT){Button=this.MouseButton.RIGHT;}else if(Button===this.MouseButton.RIGHT){Button=this.MouseButton.LEFT;} }
                                var msg = String.fromCharCode(0x00,this.InputType.MOUSE,0x00,0x0A,0x00,
                                    (Action===this.KeyAction.DBLCLICK?0x88:Action===this.KeyAction.DOWN?Button:(Button*2)&0xFF),
                                    ((X/256)&0xFF),(X&0xFF),((Y/256)&0xFF),(Y&0xFF));
                                if (this.State===3) {
                                    if (Action===this.KeyAction.NONE) { if(this.Alternate===0||this.ipad){this.send(msg);this.Alternate=1;}else{this.Alternate=0;} }
                                    else { this.send(msg); }
                                }
                            };
                        })();
                    }
                    desktop.Start(desktopNode._id);
                    desktop.latency.callback = function (ms) { /* console.log('latency', ms); */ updateSessionTime(); };
                    desktop.contype = 1;
                } else if (contype == 3) {
                    // Ask for user sessions
                    meshserver.send({ action: 'msg', type: 'userSessions', nodeid: currentNode._id, tag: consent });
                } else if (contype == 4) {
                    // Setup RDP remote desktop
                    desktop = CreateRDPDesktop('Desk', domainUrl);
                    desktop.onStateChanged = onDesktopStateChange;
                    desktop.m.onScreenSizeChange = mdeskAdjust;
                    desktop.m.onClipboardChanged = function (text) { if ((text != null) && (desktopsettings.rdpautoclipboard) && (navigator.clipboard != null)) { navigator.clipboard.writeText(text).then(function () { }).catch(function (err) { console.log(err); }) } } // Put remote clipboard data into our clipboard
                    if (desktopsettings.rdpsmb) { desktop.m.SwapMouse = desktopsettings.rdpsmb; }
                    if (desktopsettings.rdprmw) { desktop.m.ReverseMouseWheel = desktopsettings.rdprmw; }
                    desktop.Start(desktopNode._id, currentNode.rdpport ? currentNode.rdpport : 3389, tsid);
                    desktop.contype = 4;
                    desktop.onConsoleMessageChange = function () {
                        if (desktop.consoleMessage) {
                            Q('p11DeskConsoleMsg').innerHTML += formatAgentConsoleMessage(desktop.consoleMessage, desktop.consoleMessageId, desktop.consoleMessageArgs);
                            QV('p11DeskConsoleMsg', true);
                            if (p11DeskConsoleMsgTimer != null) { clearTimeout(p11DeskConsoleMsgTimer); }
                            if (desktop.consoleMessageTimeout) { p11DeskConsoleMsgTimer = setTimeout(p11clearConsoleMsg, desktop.consoleMessageTimeout * 1000); }
                        } else {
                            p11clearConsoleMsg();
                        }
                    }
                }
            } else {
                // Disconnect and clean up the remote desktop
                desktop.Stop();
                webRtcDesktopReset();
                desktopNode = desktop = null;
                if (pluginHandler != null) { pluginHandler.callHook('onDesktopDisconnect'); }
            }
        }

        function askRdpCredentials() {
            if (xxdialogMode) return;
            var x = '<div>';
            if (currentNode.rdp == 1) {
                x += addHtmlFormFloating("Credenciales", '<select id=d2mode class="form-select" onchange=askRdpCredentialsValidate()><option value=1>' + "Usar credenciales del servidor" + '</option><option value=2>' + "Usar nuevas credenciales" + '</option></select>');
                x += '<div id=d2cred style=display:none>';
            } else {
                x += '<div id=d2cred>';
            }
            x += addHtmlFormFloating("Dominio", '<input type=text id=d2domain class="form-control" maxlength=128 />');
            x += addHtmlFormFloating("Nombre de Usuario", '<input type=text id=d2user class="form-control" onKeyUp=askRdpCredentialsValidate() maxlength=128 />');
            x += addHtmlFormFloating("Contraseña", '<input type=password id=d2pass class="form-control" maxlength=128 autocomplete=off />');
            if ((features2 & 0x00400000) == 0) { x += addHtmlValue("", '<label><input type="checkbox" class="form-check-input me-2" id=d2savecred>' + "Recuerda credenciales" + '</label>'); }
            x += '</div>';
            x += MoreStart();
            x += addHtmlFormFloating("Alt Shell", '<input type=text id=d2altshell class="form-control" maxlength=2048 />');
            x += addHtmlFormFloating("Dirección de trabajo", '<input type=text id=d2workdir class="form-control" maxlength=2048 />');
            x += MoreEnd();
            setModalContent('xxAddAgent', "Credenciales RDP", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => askRdpCredentialsEx());
        }

        function askRdpCredentialsValidate() {
            var ok = true;
            if (currentNode.rdp == 1) { QV('d2cred', Q('d2mode').value == 2); if (Q('d2mode').value == 1) { QE('idx_dlgOkButton', true); return; } }
            QE('idx_dlgOkButton', Q('d2user').value != '');
        }

        function askRdpCredentialsEx() {
            var width = null, height = null;
            if (desktopsettings.rdpsize) {
                if (desktopsettings.rdpsize == 'browser') {
                    width = window.innerWidth;
                    height = window.innerHeight;
                } else if (desktopsettings.rdpsize == 'screen') {
                    width = window.screen.width;
                    height = window.screen.height;
                } else if (desktopsettings.rdpsize == 'canvas') {
                    width = Q('DeskParent').offsetWidth;
                    height = Q('DeskParent').offsetHeight;
                } else {
                    var i = desktopsettings.rdpsize.indexOf('x');
                    if (i >= 1) {
                        width = parseInt(desktopsettings.rdpsize.substring(0, i));
                        height = parseInt(desktopsettings.rdpsize.substring(i + 1));
                    }
                }
            } else {
                width = Q('DeskParent').offsetWidth;
                height = Q('DeskParent').offsetHeight;
            }
            if ((currentNode.rdp == 1) && (Q('d2mode').value == 1)) {
                connectDesktop(null, 4, { servercred: true, width: width, height: height, flags: (desktopsettings.rdpflags != null) ? desktopsettings.rdpflags : 0x2F, altshell: Q('d2altshell').value, workdir: Q('d2workdir').value });
            } else {
                var savecred = false;
                if ((features2 & 0x00400000) == 0) { savecred = Q('d2savecred').checked; }
                connectDesktop(null, 4, { domain: Q('d2domain').value, username: Q('d2user').value, password: Q('d2pass').value, savecred: savecred, width: width, height: height, flags: (desktopsettings.rdpflags != null) ? desktopsettings.rdpflags : 0x2F, altshell: Q('d2altshell').value, workdir: Q('d2workdir').value });
            }
        }

        function updateMetadata(conn, elementid) {
            var str = '', viewerCount = 0;
            if (conn && (conn.State == 3)) {
                if (conn.metadata && conn.metadata.users) { for (var i in conn.metadata.users) { viewerCount += conn.metadata.users[i]; } }
                if (viewerCount > 1) { str = '<span onclick=showSessionMetadata(1) style=cursor:pointer>' + format(", {0} viendo", viewerCount) + '</span>'; }
            }
            QH('deskmetadata', str);
            if ((conn == desktop) && (xxdialogTag == ('sessionMetadata1'))) { showSessionMetadata(1); }
        }

        function showSessionMetadata(cid) {
            if (xxdialogMode && (xxdialogTag != ('sessionMetadata' + cid))) return;
            if (xxdialogMode) { setDialogMode(0); }
            var conn = null;
            if (cid == 1) { conn = desktop; }
            if (conn && conn.metadata) {
                var x = '';
                if (conn.metadata.startTime) { x += addHtmlValue4("Tiempo de Inicio", printDateTime(new Date(conn.metadata.startTime))); }
                if (conn.metadata.users) {
                    for (var i in conn.metadata.users) {
                        var val = (conn.metadata.users[i] == 1) ? "1 conexión" : format("{0} conexiones", conn.metadata.users[i]);
                        x += addHtmlValue2(getUserName(i), val);
                    }
                }
                xxdialogTag = 'sessionMetadata' + cid;
                setModalContent('xxAddAgent', "Información de la Sesión", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
        }

        function p11clearConsoleMsg() { QH('p11DeskConsoleMsg', ''); QV('p11DeskConsoleMsg', false); if (p11DeskConsoleMsgTimer) { clearTimeout(p11DeskConsoleMsgTimer); p11DeskConsoleMsgTimer = null; } }
        function p12clearConsoleMsg() { QH('p12TermConsoleMsg', ''); QV('p12TermConsoleMsg', false); if (p12TermConsoleMsgTimer) { clearTimeout(p12TermConsoleMsgTimer); p12TermConsoleMsgTimer = null; } }
        function p13clearConsoleMsg() { QH('p13FilesConsoleMsg', ''); QV('p13FilesConsoleMsg', false); if (p13FilesConsoleMsgTimer) { clearTimeout(p13FilesConsoleMsgTimer); p13FilesConsoleMsgTimer = null; } }

        function p12setConsoleMsg(msg, timeout) {
            if (msg) {
                Q('p12TermConsoleMsg').innerHTML += msg;
                QV('p12TermConsoleMsg', true);
                if (p12TermConsoleMsgTimer != null) { clearTimeout(p12TermConsoleMsgTimer); }
                if (timeout) { p12TermConsoleMsgTimer = setTimeout(p12clearConsoleMsg, timeout); }
            } else {
                p12clearConsoleMsg();
            }
        }

        function p13setConsoleMsg(msg, timeout) {
            if (msg) {
                Q('p13FilesConsoleMsg').innerHTML += msg;
                QV('p13FilesConsoleMsg', true);
                if (p13FilesConsoleMsgTimer != null) { clearTimeout(p13FilesConsoleMsgTimer); }
                if (timeout) { p13FilesConsoleMsgTimer = setTimeout(p13clearConsoleMsg, timeout); }
            } else {
                p13clearConsoleMsg();
            }
        }

        var webRtcDesktop = null;
        function webRtcDesktopReset() {
            if (webRtcDesktop == null) return;
            if (webRtcDesktop.softdesktop != null) { webRtcDesktop.softdesktop.Stop(); webRtcDesktop.softdesktop = null; }
            if (webRtcDesktop.webchannel != null) { try { webRtcDesktop.webchannel.close(); } catch (e) { } webRtcDesktop.webchannel = null; }
            if (webRtcDesktop.webrtc != null) { try { webRtcDesktop.webrtc.close(); } catch (e) { } webRtcDesktop.webrtc = null; }
            webRtcDesktop = null;
            // Switch back to hardware KVM
            if (desktop && desktop.m) {
                desktop.m.hold(false);
                Q('deskstatus').textContent = StatusStrs[desktop.State];
            }
            // ###BEGIN###{DesktopInbandFiles}
            /*
            p24files = null;
            p24downloadFileCancel() // If any downloads are in process, cancel them.
            p24uploadFileCancel(); // If any uploads are in process, cancel them.
            QV('go24', false); // Files
            if (currentView == 24) { go(14); }
            */
            // ###END###{DesktopInbandFiles}
        }

        function onDesktopStateChange(xdesktop, state) {
            var xstate = state;
            if ((xstate == 3) && (xdesktop.contype == 2)) { xstate++; }
            var str = StatusStrs[xstate];
            if ((desktop != null) && (desktop.webRtcActive == true)) { str += ", WebRTC"; }
            if ((desktop != null) && (xstate == 3) && (desktop.contype == 4)) { str += ", RDP"; }
            //if (desktop.m.stopInput == true) { str += ', Loopback'; }
            QH('deskstatus', str);
            switch (state) {
                case 0:
                    if (desktop != null) {
                        // Stop recording
                        if (desktop.m.recordedData != null) { deskRecordSession(); }
                        // Disconnect and clean up the remote desktop
                        desktop.Stop();
                    }
                    desktopNode = desktop = null;
                    QV('DeskFocus', false);
                    QV('DeskMonitorSelectionSpan', false);
                    QV('deskMobileMonitorRow', false);
                    QV('deskRecordIcon', false);
                    QV('DeskInputLockedButton', false);
                    QV('DeskInputUnLockedButton', false);
                    deskFocusBtn.value = "Todos se Centran";
                    if (fullscreen == true) { deskToggleFull(); }
                    _mobileAutoFullscreen = false; // reset so reconnecting in landscape re-triggers
                    if (typeof _mobileModifiers !== 'undefined') { for (var _m in _mobileModifiers) _mobileModifiers[_m] = false; _updateMobileModifierUI(); }
                    // Reset trackpad and soft keyboard state on disconnect
                    if (typeof trackpadMode !== 'undefined') {
                        trackpadMode = false;
                        var _tpc = document.getElementById('trackpadCursor');
                        if (_tpc) _tpc.style.display = 'none';
                    }
                    if (typeof _mobileKbdEnabled !== 'undefined') {
                        _mobileKbdEnabled = false;
                        var _sk = document.getElementById('softKeyboard');
                        if (_sk) _sk.blur();
                        _updateSoftKbdIcon();
                    }
                    webRtcDesktopReset();
                    deskPreferedStickyDisplay = 0;
                    break;
                case 2:
                    break;
                case 3:
                    if (desktop && (desktop.serverIsRecording == true)) { QV('deskRecordIcon', true); }
                    desktop.startTime = new Date();
                    deskLastClipboardReceived = null;
                    // Pre-seed sent value with current local clipboard so we don't blast it to the remote on connect
                    if ((navigator.clipboard != null) && (navigator.clipboard.readText != null) && (Mstsc.browser() != 'firefox')) {
                        try {
                            navigator.clipboard.readText().then(function (text) { deskLastClipboardSent = text; }).catch(function (err) { deskLastClipboardSent = null; });
                        } catch (ex) { deskLastClipboardSent = null; }
                    } else {
                        deskLastClipboardSent = null;
                    }
                    if (updateSessionTimer == null) { updateSessionTimer = setInterval(updateSessionTime, 1000); }
                    // On mobile, wake the agent to start streaming.
                    // SendRefresh alone is sometimes ignored before the first mouse event.
                    // Synthetic mousedown+mouseup guarantees the agent starts sending frames.
                    if (document.body.classList.contains('is-mobile')) {
                        // Re-trigger landscape fullscreen if phone is already in landscape
                        // when reconnecting (no orientationchange event fires in this case)
                        setTimeout(function() { if (typeof _doOrientationUpdate === 'function') _doOrientationUpdate(); }, 600);
                        setTimeout(function() {
                            if (!desktop || desktop.State !== 3) return;
                            desktop.m.SendRefresh();
                            var dm = desktop.m;
                            if (dm && dm.CanvasId) {
                                var r = dm.CanvasId.getBoundingClientRect();
                                if (r.width > 0) {
                                    var ev = { pageX: r.left + window.scrollX + r.width / 2, pageY: r.top + window.scrollY + r.height / 2, button: 0, buttons: 0, which: 1 };
                                    dm.mousedown(ev);
                                    dm.mouseup(ev);
                                }
                            }
                        }, 500);
                    }
                    break;
                default:
                    //console.log('Unknown onDesktopStateChange state', state);
                    break;
            }
            updateDesktopButtons();
            deskAdjust();
            setTimeout(deskAdjust, 50);
            updateMetadata(desktop, 'deskmetadata');
        }

        function updateSessionTime() {
            // Desktop
            var latencyStr = '', seconds = 0;
            if (desktop && desktop.startTime) {
                if (desktop.latency && (desktop.latency.current >= 0)) { latencyStr = format('{0} ms', desktop.latency.current); }
                seconds = Math.floor((new Date() - desktop.startTime) / 1000);
                QH('DeskTimer', zeroPad(Math.floor(seconds / 3600), 2) + ':' + zeroPad((Math.floor(seconds / 60) % 60), 2) + ':' + zeroPad((seconds % 60), 2));
                QH('DeskLatency', latencyStr);
                // Auto-clipboard
                var autoClipEnabled = ((desktop.contype != 4) && (desktopsettings.autoclipboard === true)) || ((desktop.contype == 4) && (desktopsettings.rdpautoclipboard === true));
                if (autoClipEnabled && (navigator.clipboard != null)) {
                    // Local -> Remote: skip on Firefox as readText triggers an annoying paste popup every second
                    if ((Mstsc.browser() != 'firefox') && (navigator.clipboard.readText != null)) {
                        try {
                            navigator.clipboard.readText().then(function (text) {
                                if (desktop == null) return;
                                if ((text != null) && (deskLastClipboardSent != text)) {
                                    if (desktop.m.setClipboard) {
                                        desktop.m.setClipboard(text);
                                    } else {
                                        meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text });
                                    }
                                    deskLastClipboardSent = text;
                                }
                            }).catch(function (err) { });
                        } catch (ex) { console.log(ex); }
                    }
                    // Remote -> Local: poll remote clipboard every second (non-RDP only; RDP uses onClipboardChanged callback)
                    if ((desktop.contype != 4) && (currentNode != null) && (navigator.clipboard.writeText != null)) {
                        meshserver.send({ action: 'msg', type: 'getclip', nodeid: currentNode._id, tag: 3 });
                    }
                }
            } else {
                QH('DeskTimer', '');
                QH('DeskLatency', '');
            }

            // Terminal
            seconds = 0;
            if (terminal && terminal.startTime) {
                if (terminal.latency && (terminal.latency.current >= 0)) { latencyStr = format('{0} ms, ', terminal.latency.current); }
                seconds = Math.floor((new Date() - terminal.startTime) / 1000);
                QH('TermTimer', latencyStr + zeroPad(Math.floor(seconds / 3600), 2) + ':' + zeroPad((Math.floor(seconds / 60) % 60), 2) + ':' + zeroPad((seconds % 60), 2));
            } else {
                QH('TermTimer', '');
            }

            if ((desktop == null) && (terminal == null)) { clearInterval(updateSessionTimer); updateSessionTimer = null; }
        }

        function showDesktopSettings() {
            if (xxdialogMode) return;
            applyDesktopSettings();
            updateDesktopButtons();

            var tabSelected = false; // See if a visible tab is currently selected
            if ((QS('td7meshkvm').display != 'none') && (Q('td7meshkvm').className.indexOf('active') > 0)) { tabSelected = true; }
            else if ((QS('td7rdpkvm').display != 'none') && (Q('td7rdpkvm').className.indexOf('active') > 0)) { tabSelected = true; }
            else if ((QS('td7amtkvm').display != 'none') && (Q('td7amtkvm').className.indexOf('active') > 0)) { tabSelected = true; }
            if (tabSelected == false) { // if not, select the first visible one
                if (QS('td7meshkvm').display != 'none') { changeDesktopSettingsTab(null, 'd7meshkvm'); }
                else if (QS('td7rdpkvm').display != 'none') { changeDesktopSettingsTab(null, 'd7rdpkvm'); }
                else if (QS('td7amtkvm').display != 'none') { changeDesktopSettingsTab(null, 'd7amtkvm'); }
            }

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Opciones de Escritorio Remoto", 7);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', showDesktopSettingsChanged);
        }

        function changeDesktopSettingsTab(evt, tabname) {
            var i, dialog, tabcontent, tablinks;

            dialog = Q('dialog7');

            tabcontent = dialog.getElementsByClassName('tab-pane');
            for (i = 0; i < tabcontent.length; i++) { tabcontent[i].className = tabcontent[i].className.replace(' active', ''); }

            tablinks = dialog.getElementsByClassName('nav-link');
            for (i = 0; i < tablinks.length; i++) { tablinks[i].className = tablinks[i].className.replace(' active', ''); }

            Q(tabname).className += ' active';
            Q('t' + tabname).className += ' active';
        }

        function showDesktopSettingsChanged() {
            desktopsettings.encoding = d7desktopmode.value;
            desktopsettings.showfocus = d7showfocus.checked;
            desktopsettings.showmouse = d7showcursor.checked;
            desktopsettings.quality = d7bitmapquality.value;
            desktopsettings.scaling = d7bitmapscaling.value;
            desktopsettings.framerate = d7framelimiter.value;
            desktopsettings.agentencoding = d7encoding.value;
            desktopsettings.swapmouse = d7deskSwapMouse.checked;
            desktopsettings.rmw = d7deskrmw.checked;
            desktopsettings.remotekeymap = d7deskRemoteKeyMap.checked;
            desktopsettings.autoclipboard = d7deskAutoClipboard.checked;
            desktopsettings.autolock = d7deskAutoLock.checked;
            desktopsettings.localkeymap = d7localKeyMap.checked;
            desktopsettings.kvmrmw = d7kvmrmw.checked;
            desktopsettings.rdpsize = d7rdpsize.value;
            desktopsettings.rdpsmb = d7rdpsmb.checked;
            desktopsettings.rdprmw = d7rdprmw.checked;
            desktopsettings.rdpautoclipboard = d7rdpclip.checked;
            var rdpflags = 0;
            for (var i = 1; i < 10; i++) { if ((i != 5) && (Q('d7rdp' + i).checked)) { rdpflags |= (1 << (i - 1)); } }
            desktopsettings.rdpflags = rdpflags;
            putstore('desktopsettings', JSON.stringify(desktopsettings));
            applyDesktopSettings();
            updateDesktopButtons();
            if (desktop) {
                if (desktop.contype == 1) { // Mesh Agent Remote Desktop
                    desktop.m.SwapMouse = desktopsettings.swapmouse;
                    desktop.m.ReverseMouseWheel = desktopsettings.rmw;
                    desktop.m.remoteKeyMap = desktopsettings.remotekeymap;
                    if (desktop.State != 0) {
                        desktop.m.SendCompressionLevel(desktopsettings.agentencoding, desktopsettings.quality, desktopsettings.scaling, desktopsettings.framerate);
                        desktop.sendCtrlMsg('{"ctrlChannel":"102938","type":"autolock","value":' + desktopsettings.autolock + '}');
                        desktop.m.SendRefresh();
                    }
                }
                if (desktop.contype == 2) { // Intel AMT KVM
                    desktop.m.ReverseMouseWheel = desktopsettings.kvmrmw;
                    if (desktopsettings.showfocus == false) { desktop.m.focusmode = 0; deskFocusBtn.value = "Todos se Centran"; }
                    if (desktop.State != 0) { desktop.Stop(); setTimeout(function () { connectDesktop(null, 2); }, 50); }
                }
                if (desktop.contype == 4) { // Web-RDP
                    desktop.m.SwapMouse = desktopsettings.rdpsmb;
                    desktop.m.ReverseMouseWheel = desktopsettings.rdprmw;
                }
            }
        }

        function applyDesktopSettings() {
            var r = '', ops = (features & 512) ? [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 5, 1] : [60, 50, 40, 30, 20, 10, 5, 1];
            for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
            QH('d7bitmapquality', r);
            d7desktopmode.value = desktopsettings.encoding;
            d7showfocus.checked = desktopsettings.showfocus;
            d7showcursor.checked = desktopsettings.showmouse;
            if (desktopsettings.agentencoding) { d7encoding.value = desktopsettings.agentencoding; } else { desktopsettings.agentencoding = 4; }
            d7bitmapquality.value = 40; // Default value
            if (ops.indexOf(parseInt(desktopsettings.quality)) >= 0) { d7bitmapquality.value = desktopsettings.quality; }
            d7bitmapscaling.value = desktopsettings.scaling;
            if (desktopsettings.framerate) { d7framelimiter.value = desktopsettings.framerate; } else { d7framelimiter.value = 100; }
            if (desktopsettings.swapmouse != null) { d7deskSwapMouse.checked = desktopsettings.swapmouse; }
            if (desktopsettings.rmw != null) { d7deskrmw.checked = desktopsettings.rmw; }
            if (desktopsettings.remotekeymap != null) { d7deskRemoteKeyMap.checked = desktopsettings.remotekeymap; }
            if (desktopsettings.autoclipboard != null) { d7deskAutoClipboard.checked = desktopsettings.autoclipboard; }
            if (desktopsettings.autolock != null) { d7deskAutoLock.checked = desktopsettings.autolock; }
            if (desktopsettings.localkeymap) { d7localKeyMap.checked = desktopsettings.localkeymap; }
            if (desktopsettings.kvmrmw) { d7kvmrmw.checked = desktopsettings.kvmrmw; }
            QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (desktop.state != 0) && (desktopsettings.showfocus));
            if (desktopsettings.rdpsize != null) { d7rdpsize.value = desktopsettings.rdpsize; }
            if (desktopsettings.rdpflags == null) { desktopsettings.rdpflags = 0x2F; }
            if (desktopsettings.rdpsmb != null) { d7rdpsmb.checked = desktopsettings.rdpsmb; }
            if (desktopsettings.rdprmw != null) { d7rdprmw.checked = desktopsettings.rdprmw; }
            if (desktopsettings.rdpautoclipboard != null) { d7rdpclip.checked = desktopsettings.rdpautoclipboard; }
            for (var i = 1; i < 10; i++) { if (i != 5) { Q('d7rdp' + i).checked = ((desktopsettings.rdpflags & (1 << (i - 1))) != 0); } }
        }

        // Enter browser fullscreen
        function enterBrowserFullscreen(elem) {
            if (navigator.keyboard && navigator.keyboard.lock) { navigator.keyboard.lock(); }
            if (elem.requestFullscreen) { elem.requestFullscreen(); }
            else if (elem.msRequestFullscreen) { elem.msRequestFullscreen(); }
            else if (elem.mozRequestFullScreen) { elem.mozRequestFullScreen(); }
            else if (elem.webkitRequestFullscreen) { elem.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); }
        }

        // Exit browser fullscreen
        function exitBrowserFullscreen() {
            if (document.exitFullscreen) { document.exitFullscreen(); }
            else if (document.msExitFullscreen) { document.msExitFullscreen(); }
            else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); }
            else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); }
            if (navigator.keyboard && navigator.keyboard.unlock) { navigator.keyboard.unlock(); }
        }

        // Return true if the browser is fullscreen. This is a delayed method that will return true/false late. Not very useful.
        function isBrowserFullscreen() {
            if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { return false; } else { return true; }
        }

        var fullscreen = false;
        var browserfullscreen = false;
        function deskToggleFull(e) {
            var xtermActive = !((args.xterm === 0) || ((terminal != null) && (xterm == null)));
            fullscreen = !fullscreen;
            if (fullscreen) {
                QC('body').add('fulldesk');
                QS('deskarea3x')['height'] = '100%';
                QS('deskarea3x')['max-height'] = '100%';
                if (xtermActive) {
                    // XTerm terminal
                    QS('termTable')['position'] = 'absolute';
                    QS('termTable')['top'] = QS('termTable')['bottom'] = QS('termTable')['left'] = QS('termTable')['right'] = '0';
                } else {
                    // Legacy terminal
                    QS('termTable')['height'] = '100%';
                    QS('termTable')['max-height'] = '100%';
                }

                // On mobile or when shift is pressed, enter browser full screen.
                if (e.shiftKey == true || document.body.classList.contains('is-mobile')) {
                    enterBrowserFullscreen(Q('body'));
                    browserfullscreen = true;
                }
            } else {
                QC('body').remove('fulldesk');
                var hide = args.hide;
                if (footerBar == false) { hide |= 4; }
                var xh = (((hide & 1) ? 0 : 66) + ((hide & 2) ? 0 : 24) + ((hide & 4) ? 0 : 45) + ((hide & 8) ? 0 : 60)); // 0 to 195
                QS('deskarea3x')['height'] = 'calc(100vh - ' + (75 + xh) + 'px)';
                QS('deskarea3x')['max-height'] = 'calc(100vh - ' + (75 + xh) + 'px)';
                if (xtermActive) {
                    // XTerm terminal
                    QS('termTable')['position'] = null;
                    QS('termTable')['top'] = QS('termTable')['bottom'] = QS('termTable')['left'] = QS('termTable')['right'] = null;
                } else {
                    // Legacy terminal
                    QS('termTable')['height'] = 'calc(100vh - ' + (75 + xh) + 'px)';
                    QS('termTable')['max-height'] = 'calc(100vh - ' + (75 + xh) + 'px)';
                }
                if (browserfullscreen == true) { exitBrowserFullscreen(); browserfullscreen = false; }
            }
            deskAdjust();
            updateDesktopButtons();
            adjustPanels();
            //setTimeout(adjustPanels, 10);
            //setTimeout(function() { xtermfit.fit(); }, 10);
            if (xterm != null) { if (xxcurrentView == 12) { xtermfit.fit(); xterm.focus(); } }
        }

        function deskToggleFocus() {
            desktop.m.focusmode = (desktop.m.focusmode + 64) % 192;
            Q('deskFocusBtn').value = ["Todos se Centran", "Enfoque Pequeño", "Gran Enfoque"][desktop.m.focusmode / 64];
        }

        function deskAdjust() {
            //if (xxcurrentView == 1) return;
            var parentH = Q('DeskParent').clientHeight, parentW = Q('DeskParent').clientWidth;
            var deskH = Q('Desk').height, deskW = Q('Desk').width;

            if (deskAspectRatio == 2) {
                // Scale mode
                QS('Desk')['margin-top'] = null;
                QS('Desk').height = '100%';
                QS('Desk').width = '100%';
                //QS('deskarea3x').height = null;
                QS('DeskParent').overflow = 'hidden';
            } else if (deskAspectRatio == 1) {
                // Zoomed mode
                QS('Desk')['margin-top'] = '0px';
                QS('Desk').height = deskH + 'px';
                QS('Desk').width = deskW + 'px';
                QS('DeskParent').overflow = 'scroll';
            } else {
                // Fixed aspect ratio
                if ((parentH / parentW) > (deskH / deskW)) {
                    var hNew = ((deskH * parentW) / deskW) + 'px';
                    //if (webPageFullScreen || fullscreen) {
                    //  QS('deskarea3x').height = null;
                    //} else {
                    //  QS('deskarea3x').height = hNew;
                    //  QS('deskarea3x').height = null;
                    //}
                    QS('Desk').height = hNew;
                    QS('Desk').width = '100%';
                } else {
                    var wNew = ((deskW * parentH) / deskH) + 'px';
                    if (webPageFullScreen || fullscreen) {
                        QS('Desk').height = null;
                    } else {
                        QS('Desk').height = '100%';
                    }
                    QS('Desk').width = wNew;
                }
                QS('Desk')['margin-top'] = null;
                QS('DeskParent').overflow = 'hidden';
            }
        }

        function mdeskAdjust(module, sw, sh, cv) {
            if (!module || !sw || !sh || !cv) return;
            var view = Q('viewselect').value;
            if ((view != 3) && (view != 5)) return;

            // Check if we are in single desktop mode
            if (cv.id == 'Desk') { deskAdjust(); return; }

            if (view == 5) { // If not in fixed width mode, compute the with
                var vsize = [{ x: 180, y: 101 }, { x: 302, y: 169 }, { x: 454, y: 255 }][Q('sizeselect').selectedIndex];
                QS(cv.id)['width'] = ((module.State != 0) ? ((sw * vsize.y) / sh) : (vsize.x)) + 'px';
            }
            QS(cv.id)['margin-top'] = null;
            QS(cv.id)['margin-bottom'] = null;
        }

        // Press ESC key
        function sendDeskEsc() {
            if (desktop == null || desktop.State != 3) return;
            if (desktop.contype == 2) {
                desktop.m.sendkey([[65307, 1], [65307, 0]]); // Intel AMT: ESC down, ESC release
            } else {
                desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 27], [desktop.m.KeyAction.EXUP, 27]]); // MeshAgent: ESC press, ESC release
            }
        }

        function p11fileDragDrop(e) {
            haltEvent(e);
            QV('p11bigfail', false);
            QV('p11bigok', false);
            if ((xxdialogMode != null) || (desktop == null) || (desktop.State != 3) || (e.dataTransfer == null) || (e.dataTransfer.files.length == 0)) return;

            var file = e.dataTransfer.files[0];
            var filename = file.name.toLowerCase();
            if (filename.endsWith('.bat') || filename.endsWith('.ps1') || filename.endsWith('.sh') || filename.endsWith('.agentconsole')) {
                d2runCommandDialog({ nodeids: [currentNode._id], selectedFile: file });
            } else if (filename.endsWith('.txt')) {
                var reader = new FileReader();
                reader.onload = function (e) { showDeskType(e.target.result); }
                reader.readAsText(file);
            }
        }

        var p11dragtimer = null;
        function p11fileDragOver(e) {
            haltEvent(e);
            if (p11dragtimer != null) { clearTimeout(p11dragtimer); p11dragtimer = null; }
            var ac = ((xxdialogMode == null) && (desktop != null) && (desktop.State == 3));
            QV('p11bigok', ac);
            QV('p11bigfail', !ac);
        }

        function p11fileDragLeave(e) {
            haltEvent(e);
            if (e.target.id != 'Desk') {
                QV('p11bigfail', false);
                QV('p11bigok', false);
            } else {
                p11dragtimer = setTimeout(function () { QV('p11bigfail', false); QV('p11bigok', false); p11dragtimer = null; }, 10);
            }
        }

        //
        // Desktop Shortcut Keys
        //

        function updateDeskShortcutKeys() {
            var x = '', v = parseInt(Q('deskkeys').value);
            if ((v == '') && (deskKeyboardShortcuts.length > 0)) { v = deskKeyboardShortcuts[0]; }
            for (var i in deskKeyboardShortcuts) { x += '<option value=' + deskKeyboardShortcuts[i] + ((v == deskKeyboardShortcuts[i]) ? ' selected' : '') + '>' + keyShortcutTotext(deskKeyboardShortcuts[i]) + '</option>'; }
            //x += '<option value=-1' + ((v == -1)?' selected':'') + '>' + "Customize" + '</option>';
            QH('deskkeys', x);
        }

        var keyStrings = { 8: "Retroceso", 9: "Tab", 13: "Enter", 27: "Escape", 32: "Espacio", 44: "Imprimir Pantalla", 45: "Insertar", 46: "Del", 36: "Casa", 35: "Final", 33: "Página Arriba", 34: "Página Abajo", 37: "Izquierda", 38: "Subir", 39: "Derecha", 40: "Abajo", 0: "Ninguno" }

        function keyShortcutTotext(n) {
            var x = [];
            if (n & 0x010000) { x.push("Cambiar"); }
            if (n & 0x020000) { x.push("Alt"); }
            if (n & 0x080000) { x.push("Ctrl"); }
            if (n & 0x100000) { x.push("Win"); }
            n = (n & 0xFFFF);
            if ((n >= 112) && (n <= 123)) { x.push('F' + (n - 111)); } // Fx keys
            else if ((n != 0) && (keyStrings[n])) { x.push(keyStrings[n]); }
            else { if (n != 0) { x.push(String.fromCharCode(n)); } }
            return x.join(' + ');
        }

        // Customize keyboard shortcuts
        function deskCustomizeKeys() {
            if (xxdialogMode) return;
            // Shortcut list
            var x = '<div id="d2shortcuts" class="list-group mb-3" style="max-height:200px;overflow-y:auto"></div>';
            // Modifier checkboxes
            x += '<div class="d-flex flex-wrap gap-3 mb-2">';
            x += '<div class="form-check"><input id="d1kshift" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kshift">Shift</label></div>';
            x += '<div class="form-check"><input id="d1kalt"   type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kalt">Alt</label></div>';
            x += '<div class="form-check"><input id="d1kctrl"  type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kctrl">Ctrl</label></div>';
            x += '<div class="form-check"><input id="d1kwin"   type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kwin">Win</label></div>';
            x += '</div>';
            // Key select + Add button
            x += '<div class="d-flex gap-2 mb-3">';
            x += '<select id="d2keySelect" class="form-select form-select-sm">';
            for (var i in keyStrings) { x += '<option value=' + i + '>' + keyStrings[i] + '</option>'; }
            for (var i = 1; i <= 12; i++) { x += '<option value=' + (i + 111) + '>F' + i + '</option>'; }
            for (var i = 0; i < 10; i++) { x += '<option value=' + (i + 48) + '>' + i + '</option>'; }
            for (var i = 0; i < 26; i++) { x += '<option value=' + (i + 65) + '>' + String.fromCharCode(i + 65) + '</option>'; }
            x += '</select>';
            x += '<button type="button" class="btn btn-primary btn-sm flex-shrink-0" onclick="addDeskCustomizeKey()">Add</button>';
            x += '</div>';
            // Restore defaults
            x += '<button type="button" class="btn btn-outline-secondary btn-sm w-100" onclick="restoreDeskCustomizeKey()">Restore Default Shortcuts</button>';
            setModalContent('xxAddAgent', "Atajos de teclado", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deskCustomizeKeysEx());
            deskUpdateShortcutList();
        }

        function deskCustomizeKeysEx() {
            putstore('deskKeyShortcuts', deskKeyboardShortcuts.join(','));
            updateDeskShortcutKeys();
        }

        function deskUpdateShortcutList() {
            var x = '', last = deskKeyboardShortcuts.length - 1;
            for (var i in deskKeyboardShortcuts) {
                var k = deskKeyboardShortcuts[i];
                x += '<div class="list-group-item d-flex align-items-center justify-content-between py-2 px-3 gap-2">';
                x += '<span class="flex-grow-1">' + keyShortcutTotext(k) + '</span>';
                x += '<div class="d-flex gap-1 flex-shrink-0">';
                if (parseInt(i) > 0) {
                    x += '<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deskCustomizeKeyUp(' + k + ')" title="Move up"><i class="fa-solid fa-arrow-up fa-xs"></i></button>';
                }
                if (parseInt(i) < last) {
                    x += '<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deskCustomizeKeyDown(' + k + ')" title="Move down"><i class="fa-solid fa-arrow-down fa-xs"></i></button>';
                }
                x += '<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeDeskCustomizeKey(' + k + ')" title="Remove"><i class="fa-solid fa-trash fa-xs"></i></button>';
                x += '</div></div>';
            }
            if (x == '') { x = '<div class="list-group-item text-muted fst-italic">No keyboard shortcuts defined</div>'; }
            QH('d2shortcuts', x);
        }

        function deskCustomizeKeyDown(k) {
            var i = deskKeyboardShortcuts.indexOf(k), x = deskKeyboardShortcuts[i + 1];
            deskKeyboardShortcuts[i + 1] = deskKeyboardShortcuts[i];
            deskKeyboardShortcuts[i] = x;
            deskUpdateShortcutList();
        }

        function deskCustomizeKeyUp(k) {
            var i = deskKeyboardShortcuts.indexOf(k), x = deskKeyboardShortcuts[i];
            deskKeyboardShortcuts[i] = deskKeyboardShortcuts[i - 1];
            deskKeyboardShortcuts[i - 1] = x;
            deskUpdateShortcutList();
        }

        function removeDeskCustomizeKey(k) {
            var na = [];
            for (var i in deskKeyboardShortcuts) { if (deskKeyboardShortcuts[i] != k) { na.push(deskKeyboardShortcuts[i]); } }
            deskKeyboardShortcuts = na;
            deskUpdateShortcutList();
        }

        function restoreDeskCustomizeKey() {
            deskKeyboardShortcuts = [];
            putstore('deskKeyShortcuts', null);
            var deskKeyboardShortcutsStr = getstore('deskKeyShortcuts', '0x0A002E,0x100000,0x100028,0x100026,0x10004C,0x10004D,0x11004D,0x100052,0x020073,0x080057,0x020009,0x100025,0x100027').split(',');
            for (var i in deskKeyboardShortcutsStr) { if (deskKeyboardShortcutsStr[i] != "") { deskKeyboardShortcuts.push(parseInt(deskKeyboardShortcutsStr[i])); } }
            updateDeskShortcutKeys();
            deskUpdateShortcutList();
        }

        function addDeskCustomizeKey() {
            var k = parseInt(Q('d2keySelect').value);
            if (Q('d1kshift').checked) { k |= 0x010000; }
            if (Q('d1kalt').checked) { k |= 0x020000; }
            if (Q('d1kctrl').checked) { k |= 0x080000; }
            if (Q('d1kwin').checked) { k |= 0x100000; }
            if ((k > 0) && (deskKeyboardShortcuts.indexOf(k) == -1)) {
                deskKeyboardShortcuts.push(k);
                deskUpdateShortcutList();
                // Reset form so the next shortcut starts fresh
                Q('d1kshift').checked = false;
                Q('d1kalt').checked = false;
                Q('d1kctrl').checked = false;
                Q('d1kwin').checked = false;
                Q('d2keySelect').selectedIndex = 0;
            }
        }

        // Customize keyboard strings
        function deskCustomizeStrings() {
            if (xxdialogMode) return;
            var x = '<div id=d2strings style="width:100%;height:180px;padding:4px;overflow-y:auto;border:1px solid gray"></div><div style=width:100%;padding:5px>';
            x += addHtmlFormFloating("Nombre", '<input type=text id=d2shortcutname class="form-control" maxlength=32 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating("Valor", '<input type=text id=d2shortcutvalue class="form-control" maxlength=256 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating('', '<input id=d2addbutton type=button value=' + "Agregar" + ' onclick=addDeskCustomizeString() class="form-control" />');
            x + '</div>';
            setModalContent('xxAddAgent', "Personalización de cuerdas de teclado", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deskCustomizeStringsEx());
            deskUpdateStringsList();
            deskCustomizeStringsUpdate();
        }

        function deskCustomizeStringsUpdate() {
            QE('d2addbutton', ((Q('d2shortcutname').value != '') && (Q('d2shortcutvalue').value != '')));
        }

        function deskCustomizeStringsEx() {
            putstore('deskStrings', JSON.stringify(deskKeyboardStrings));
            updateDesktopStrings();
        }

        function deskUpdateStringsList() {
            var x = '';
            for (var i in deskKeyboardStrings) {
                var orderButtons = '';
                if (i != (deskKeyboardStrings.length - 1)) { orderButtons += '<img width=8 height=8 style=float:right;cursor:pointer;padding:3px src="images/c2.png" onclick=deskCustomizeStringDown(' + i + ')>'; }
                if (i != 0) { orderButtons += '<img width=8 height=8 style=float:right;cursor:pointer;padding:3px src="images/c3.png" onclick=deskCustomizeStringUp(' + i + ')>'; }
                x += '<div style="width:100%;background-color:#AAA;border-radius:4px;margin-bottom:4px;padding:4px;text-align:left;box-sizing:border-box" value=' + (i + 1000) + '><div><b>' + EscapeHtml(deskKeyboardStrings[i].n) + '</b><img width=10 height=10 style=float:right;cursor:pointer;padding:2px;margin-left:8px src="images/trash.png" onclick=removeDeskCustomizeString(' + i + ')>' + orderButtons + '</div>';
                x += '<div stlye=font-size:x-small>' + EscapeHtml(deskKeyboardStrings[i].v) + '</div>';
                x += '</div>';
            }
            if (x == '') { x = '<i>' + "No hay cadenas de teclado definidas" + '</i>'; }
            QH('d2strings', x);
        }

        function deskCustomizeStringDown(i) {
            var x = deskKeyboardStrings[i + 1];
            deskKeyboardStrings[i + 1] = deskKeyboardStrings[i];
            deskKeyboardStrings[i] = x;
            deskUpdateStringsList();
        }

        function deskCustomizeStringUp(i) {
            var x = deskKeyboardStrings[i];
            deskKeyboardStrings[i] = deskKeyboardStrings[i - 1];
            deskKeyboardStrings[i - 1] = x;
            deskUpdateStringsList();
        }

        function removeDeskCustomizeString(i) {
            deskKeyboardStrings.splice(i, 1);
            deskUpdateStringsList();
        }

        function addDeskCustomizeString() {
            if (!Array.isArray(deskKeyboardStrings)) { deskKeyboardStrings = []; }
            var name = Q('d2shortcutname').value;
            var value = Q('d2shortcutvalue').value;
            if ((name != '') && (value != '')) { deskKeyboardStrings.push({ n: name, v: value }); deskUpdateStringsList(); }
        }

        function updateDesktopStrings() {
            var x = '';
            for (var i in deskKeyboardStrings) { var j = (parseInt(i) + 1000); x += '<div class="cmtext" onclick="cmdeskpreconfigtypeaction(' + j + ',event)">' + EscapeHtml(deskKeyboardStrings[i].n) + '</div>'; }
            if (x != '') { x += '<hr />' };
            QH('deskPreConfigShortcutContextMenu2', x);
        }

        // Remote desktop special key combos for Windows
        function deskSendKeys() {
            Q('DeskWD').blur();
            if (xxdialogMode || desktop == null || desktop.State != 3) return;

            // Construct the key command
            var ks = parseInt(Q('deskkeys').value);
            if (ks == 0x0A002E) { desktop.m.sendcad(); return; } // CTRL-ALT-DEL
            //if ((desktop.contype == 1) && (ks == 0x10004C)) { desktop.sendCtrlMsg('{"action":"lock"}'); return; } // Lock desktop, WIN + L

            var flags = (ks & 0xFF0000) >> 16, key = (ks & 0xFFFF), keyArray = [], keyArray2 = [];
            var amtTranslate = {
                8: 0xff08, // BackSpace
                9: 0xff09, // Tab
                13: 0xff0d, // Return or Enter
                27: 0xff1b, // Escape
                45: 0xff63, // Insert
                46: 0xffff, // Delete
                36: 0xff50, // Home
                35: 0xff57, // End
                33: 0xff55, // Page Up
                34: 0xff56, // Page Down
                37: 0xff51, // Left arrow
                38: 0xff52, // Up arrow
                39: 0xff53, // Right arrow
                40: 0xff54, // Down arrow
                112: 0xffbe, // F1
                113: 0xffbf, // F2
                114: 0xffc0, // F3
                115: 0xffc1, // F4
                116: 0xffc2, // F5
                117: 0xffc3, // F6
                118: 0xffc4, // F7
                119: 0xffc5, // F8
                120: 0xffc6, // F9
                121: 0xffc7, // F10
                122: 0xffc8, // F11
                123: 0xffc9  // F12
            }

            // 0x010000 = Shift
            // 0x020000 = Left-Alt
            // 0x080000 = Ctrl
            // 0x100000 = Window

            // Examples:
            // WIN+DOWN       = 0x100028
            // WIN+UP         = 0x100026
            // WIN+L          = 0x10004C
            // WIN+M          = 0x10004D
            // Shift+WIN+M    = 0x11004D
            // WIN            = 0x100000
            // WIN+R          = 0x100052
            // ALT+F4         = 0x020073
            // CTRL+W         = 0x080057
            // ALT+TAB        = 0x020009
            // CTRL-ALT-DEL   = 0x0A002E
            // WIN-LEFT       = 0x100025
            // WIN-RIGHT      = 0x100027
            // SHIFT+F10      = 0x010079

            if (desktop.contype == 2) {
                // Intel AMT
                if (flags & 1) { keyArray.push([0xffe1, 1]); keyArray2.push([0xffe1, 0]); } // Shift
                if (flags & 2) { keyArray.push([0xffe9, 1]); keyArray2.push([0xffe9, 0]); } // Left-alt
                if (flags & 8) { keyArray.push([0xffe3, 1]); keyArray2.push([0xffe3, 0]); } // Ctrl
                if (flags & 16) { keyArray.push([0xffe7, 1]); keyArray2.push([0xffe7, 0]); } // Windows key
                if (amtTranslate[key]) { key = amtTranslate[key]; }
                if ((key >= 65) && (key <= 90)) { key += 32; }
                if (key != 0) { keyArray.push([key, 1]); keyArray2.push([key, 0]); }
                keyArray2.reverse();
                for (var i = 0; i < keyArray2.length; i++) { keyArray.push(keyArray2[i]); }
                desktop.m.sendkey(keyArray);
            } else {
                // Agent desktop
                if (flags & 1) { keyArray.push([desktop.m.KeyAction.DOWN, 16]); keyArray2.push([desktop.m.KeyAction.UP, 16]); } // Shift
                if (flags & 2) { keyArray.push([desktop.m.KeyAction.EXDOWN, 18]); keyArray2.push([desktop.m.KeyAction.EXUP, 18]); } // Left-alt
                if (flags & 8) { keyArray.push([desktop.m.KeyAction.EXDOWN, 17]); keyArray2.push([desktop.m.KeyAction.EXUP, 17]); } // Ctrl
                if (flags & 16) { keyArray.push([desktop.m.KeyAction.EXDOWN, 0x5B]); keyArray2.push([desktop.m.KeyAction.EXUP, 0x5B]); } // Windows key
                if (key != 0) { keyArray.push([desktop.m.KeyAction.DOWN, key]); keyArray2.push([desktop.m.KeyAction.UP, key]); }
                keyArray2.reverse();
                for (var i = 0; i < keyArray2.length; i++) { keyArray.push(keyArray2[i]); }
                desktop.m.SendKeyMsgKC(keyArray);
            }
        }

        // Remote desktop typing
        function showDeskType(text) {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            Q('DeskType').blur();
            var x = '<div>' + "Ingresa el texto y haz clic en Aceptar para escribirlo de forma remota. Asegúrate de colocar el cursor remoto en la posición correcta antes de continuar." + '<div>';
            x += '<textarea id=d2typeText style="margin-top:5px;width:100%;height:184px;resize:none" maxlength=2000>' + (text ? EscapeHtml(text) : '') + '</textarea>';
            setModalContent('xxAddAgent', "Entrada Remota de Teclado", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showDeskTypeEx());
            Q('d2typeText').focus();
        }

        var AmtDeskTypeTimer = null;
        var AmtDeskTypeContent = null;
        var DeskTypeTranslate = { 39: 222, 42: 106, 43: 107, 44: 188, 45: 189, 46: 190, 47: 191, 59: 186, 61: 187, 91: 219, 92: 220, 93: 221, 96: 192, 191: 111 };
        var DeskTypeShiftTranslate = { 33: 49, 34: 222, 35: 51, 36: 52, 37: 53, 38: 55, 40: 57, 41: 48, 58: 186, 60: 188, 62: 190, 63: 191, 64: 50, 94: 54, 95: 189, 106: 56, 107: 187, 123: 219, 124: 220, 125: 221, 126: 192 };
        function showDeskTypeEx(text) {
            var txt, ltxt, x = [], shift = false;
            if (typeof text == 'string') { txt = text, ltxt = text.toUpperCase() } else { txt = Q('d2typeText').value, ltxt = Q('d2typeText').value.toUpperCase(); }
            if (desktop.contype == 2) {
                // Intel AMT
                for (var i in txt) { var a = txt.charCodeAt(i); x.push([a, 1], [a, 0]); }
                AmtDeskTypeContent = x;
                AmtDeskTypeTimer = setInterval(function () {
                    var key = AmtDeskTypeContent.shift();
                    if (desktop) { desktop.m.sendkey(key[0], key[1]); }
                    if ((desktop == null) || (AmtDeskTypeContent.length == 0)) { clearInterval(AmtDeskTypeTimer); AmtDeskTypeContent = null; }
                }, 10);
            } else if (desktop.contype == 4) {
                // RDP
                desktop.m.SendStringUnicode(txt);
            } else {
                // MeshAgent
                if (desktopsettings.remotekeymap !== true) {
                    // New unicode typing
                    desktop.m.SendStringUnicode(txt);
                } else {
                    // Old scan code typing. This is for non-unicode system.
                    for (var i in txt) {
                        var a = txt.charCodeAt(i), b = ltxt.charCodeAt(i);
                        if (((a >= 65) && (a <= 90)) || ((a >= 97) && (a <= 122))) {
                            if ((a == b) && (shift == false)) { x.push([desktop.m.KeyAction.DOWN, 16]); shift = true; } // LShift down
                            if ((a != b) && (shift == true)) { x.push([desktop.m.KeyAction.UP, 16]); shift = false; } // LShift up
                        } else if ((a >= 48) && (a <= 57)) {
                            if (shift == true) { x.push([desktop.m.KeyAction.UP, 16]); shift = false; } // Shift up
                        } else if (DeskTypeTranslate[a]) {
                            if (shift == true) { x.push([desktop.m.KeyAction.UP, 16]); shift = false; } // Shift up
                            b = DeskTypeTranslate[a];
                        } else if (DeskTypeShiftTranslate[a]) {
                            if (shift == false) { x.push([desktop.m.KeyAction.DOWN, 16]); shift = true; } // LShift down
                            b = DeskTypeShiftTranslate[a];
                        }
                        x.push([desktop.m.KeyAction.DOWN, b], [desktop.m.KeyAction.UP, b]);
                    }
                    if (shift == true) { x.push([desktop.m.KeyAction.UP, 16]); shift = false; } // Shift up
                    desktop.m.SendKeyMsgKC(x);
                }
            }
        }

        // Show clipboard dialog
        function showDeskClip() {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            Q('DeskClip').blur();
            var x = '<div class="container text-center"><div class="row">';
            if ((features2 & 0x0800) == 0) x += '<div class="col"><input class="btn btn-secondary" id=dlgClipGet type=button value="' + "Obtener portapapeles" + '" onclick=showDeskClipGet()></div>';
            if ((features2 & 0x1000) == 0) x += '<div class="col"><input class="btn btn-secondary" id=dlgClipSet type=button value="' + "Asignar portapapeles" + '" onclick=showDeskClipSet()></div>';
            x += '</div>';
            x += '<div class="row"><div class="col"><div id=dlgClipStatus style="display:inline-block;margin-left:8px" ></div></div></div>';
            x += '<div class="row"><div class="col"><textarea id=d2clipText style="width:100%;height:184px;resize:none" maxlength=65535></textarea></div></div>';
            x += '<div class="row"><div class="col"><div style=height:26px;margin-top:3px><span id=linuxClipWarn style=display:none>' + "El portapapeles remoto es válido por 60 segundos." + '</span></div></div></div>';
            x += '</div>'
            xxdialogTag = 'clipboard';
            setModalContent('xxAddAgent', "Portapapeles Remoto", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            Q('d2clipText').focus();
        }

        function showDeskClipGet() {
            if (desktop == null || desktop.State != 3) return;
            meshserver.send({ action: 'msg', type: 'getclip', nodeid: currentNode._id, tag: 1 });
        }

        function showDeskClipSet() {
            if (desktop == null || desktop.State != 3) return;
            if (desktop.m.setClipboard) {
                desktop.m.setClipboard(Q('d2clipText').value);
            } else {
                meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: Q('d2clipText').value });
                QV('linuxClipWarn', currentNode && currentNode.agent && (currentNode.agent.id > 4) && (currentNode.agent.id != 21) && (currentNode.agent.id != 22) && (currentNode.agent.id != 34));
            }
        }

        // Send CTRL-ALT-DEL
        function sendCAD() {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            desktop.m.sendcad();
        }

        // Show process dialogs
        function toggleDeskTools() {
            Q('DeskToolsButton').blur();
            if (xxdialogMode) return;
            if (QS('DeskTools').display == 'none') {
                QV('DeskTools', true);
                Q('DeskTools').nodeid = currentNode._id;
                QH('DeskToolsProcesses', '');
                QH('DeskToolsServices', '');
                QV('deskToolsTopTabService', false);
                changeDeskToolTab(0)
                refreshDeskTools(0);
                refreshDeskTools(1);
            } else {
                QV('DeskTools', false);
            }
        }

        var deskToolTabSelection = 0;
        function changeDeskToolTab(tabnum) {
            deskToolTabSelection = tabnum;
            QV('DeskToolsProcessTab', tabnum == 0);
            QV('DeskToolsServiceTab', tabnum == 1);
            QS('deskToolsTopTabProcess')['bottom'] = (tabnum == 0) ? '0px' : '3px';
            QS('deskToolsTopTabService')['bottom'] = (tabnum == 1) ? '0px' : '3px';
            QS('deskToolsTopTabProcess')['color'] = (tabnum == 0) ? 'black' : 'gray';
            QS('deskToolsTopTabService')['color'] = (tabnum == 1) ? 'black' : 'gray';
        }

        // Refresh all of the desktop tool panels
        function refreshDeskTools(x) {
            var sel = (x == null) ? deskToolTabSelection : x;
            QV('DeskToolsRefreshButton', false);
            setTimeout(refreshDeskToolsEx, 500);
            if (sel == 0) meshserver.send({ action: 'msg', type: 'ps', nodeid: currentNode._id });
            if (sel == 1) meshserver.send({ action: 'msg', type: 'services', nodeid: currentNode._id });
        }
        function refreshDeskToolsEx() { QV('DeskToolsRefreshButton', true); }
        var deskTools = { sort: 1, ssort: 1, msg: null, smsg: null, resizing: false };
        Q('DeskTools').addEventListener('mousemove', function (event) {
            var mouseX = event.clientX - Q('DeskTools').getBoundingClientRect().left;
            var borderThickness = 5; // Assuming a 5px border
            if (mouseX < borderThickness) {
                Q('DeskTools').style.cursor = 'col-resize';
            } else {
                Q('DeskTools').style.cursor = 'default';
            }
        });
        Q('DeskTools').addEventListener('mousedown', function (event) {
            if (Q('DeskTools').style.cursor === 'col-resize') {
                deskTools.resizing = true;
                Q('Desk').removeAttribute('onmouseup');
                Q('Desk').removeAttribute('onmousedown');
                Q('Desk').removeAttribute('onmousemove');
            }
        });
        document.addEventListener('mouseup', function () {
            if (deskTools.resizing === true && Q('DeskTools').style.cursor === 'col-resize') deskTools.resizing = false;
        });
        document.addEventListener('mousemove', function (event) {
            if (deskTools.resizing) {
                var newWidth = Q('DeskTools').getBoundingClientRect().right - event.clientX;
                if (newWidth < (Q('DeskParent').clientWidth - 10)) Q('DeskTools').style.width = newWidth + 'px';
            }
        });
        function sortProcess(sort) { deskTools.sort = sort; showDeskToolsProcesses(deskTools.msg); }
        function sortService(sort) { deskTools.ssort = sort; showDeskToolsServices(deskTools.smsg); }
        function sortProcessPid(a, b) { if (a.p > b.p) return 1; if (a.p < b.p) return (-1); return sortProcessName(a, b); }
        function sortProcessName(a, b) { if (a.d > b.d) return 1; if (a.d < b.d) return (-1); return 0; }
        function showDeskToolsProcesses(message) {
            deskTools.msg = message;
            if (message == null) { QH('DeskToolsProcesses', ''); return; }
            if (Q('DeskTools').nodeid != message.nodeid) return;
            var p = [], processes = null;
            try { processes = JSON.parse(message.value); } catch (e) { }
            if (processes != null) {
                for (var pid in processes) { p.push({ p: parseInt(pid), c: processes[pid].cmd, d: processes[pid].cmd.toLowerCase(), u: processes[pid].user }); }
                if (deskTools.sort == 0) { p.sort(sortProcessPid); } else if (deskTools.sort == 1) { p.sort(sortProcessName); }
                var x = '';
                for (var i in p) {
                    if (p[i].p != 0) {
                        var c = p[i].c;
                        x += '<div onclick=showProcessDetails(' + p[i].p + ') class="deskToolsBar d-flex">';
                        x += '<div class="pe-1 text-start" style=width:50px;>' + EscapeHtml(p[i].p) + '</div>';
                        x += '<div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="' + EscapeHtml(c) + '">' + c + '</div>';
                        x += '<div class="ms-auto">' + (p[i].u ? EscapeHtml(p[i].u) : '');
                        x += '<i class="ps-1 fa-solid fa-trash-can" role="button" title="' + "Detener proceso" + '" onclick=\'return stopProcess(' + EscapeHtml(p[i].p) + ',"' + EscapeHtml(p[i].c) + '")\'></i></div>';
                        x += '</div>';
                    }
                }
                QH('DeskToolsProcesses', x);
            }
        }
        function showProcessDetails(pid) {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', format("Detalles del Proceso, #{0}", pid), 'Requesting Process Details...');
            xxdialogTag = 'ps|' + currentNode._id + '|' + pid;
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            meshserver.send({ action: 'msg', type: 'psinfo', nodeid: currentNode._id, pid: pid });
        }
        function showDeskToolsServices(message) {
            deskTools.smsg = message;
            if (message == null) { QH('DeskToolsProcesses', ''); return; }
            if (Q('DeskTools').nodeid != message.nodeid) return;
            QV('deskToolsTopTabService', true);
            var s = [], services = null;
            try { services = JSON.parse(message.value); } catch (e) { }
            deskTools.services = services;
            if (services != null) {
                for (var i in services) {
                    if (services[i].status) {
                        // Windows
                        s.push({ p: capitalizeFirstLetter(services[i].status.state.toLowerCase()), d: services[i].displayName, i: i });
                    } else if (services[i].serviceType) {
                        // Linux (TODO: This the service status is not displayed, not sure start/stop/restart will work).
                        s.push({ p: services[i].serviceType, d: services[i].name, i: i });
                    }
                }
                if (deskTools.ssort == 0) { s.sort(sortProcessPid); } else if (deskTools.ssort == 1) { s.sort(sortProcessName); }
                var x = '';
                for (var i in s) {
                    if (s[i].p != 0) {
                        var c = s[i].d, ss = s[i].p;
                        if (ss == 'Stopped') { ss = "Detenido"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Ejecutando"; }
                        x += '<div onclick=showServiceWaitDialog(' + s[i].i + ') class=deskToolsBar>';
                        x += '<div style=width:70px;float:left;padding-right:5px>' + EscapeHtml(ss) + '</div>';
                        x += '<div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="' + c + '">' + EscapeHtml(c) + '</div>';
                        x += '</div>';
                    }
                }
                QH('DeskToolsServices', x);
            }
        }

        function showServiceDetailsDialog(data) {
            if (xxdialogMode && (xxdialogTag.indexOf('service_') != -1)) {
                var index = xxdialogTag.replace('service_', '')
                // var org_service = deskTools.services[index];
                var service = data.value ? JSON.parse(data.value) : deskTools.services[index];
                if (data.value) service.displayName = deskTools.services[index].displayName;
                if (service != null) {
                    var x = '';
                    if (service.name) { x += addHtmlValue("Nombre", service.name); }
                    if (service.displayName) { x += addHtmlValue("Nombre para mostrar", service.displayName); }
                    if (service.status) {
                        var ss = capitalizeFirstLetter(service.status.state.toLowerCase());
                        if (ss == 'Stopped') { ss = "Detenido"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Ejecutando"; }
                        if (service.status.state) { x += addHtmlValue("Estado", ss); }
                        if (service.status.pid) { x += addHtmlValue("PID", service.status.pid); }
                        var serviceTypes = [];
                        if (service.status.isFileSystemDriver === true) { serviceTypes.push("FileSystemDriver"); }
                        if (service.status.isInteractive === true) { serviceTypes.push("Interactivo"); }
                        if (service.status.isKernelDriver === true) { serviceTypes.push("KernelDriver"); }
                        if (service.status.isOwnProcess === true) { serviceTypes.push("Tomar Proceso"); }
                        if (service.status.isSharedProcess === true) { serviceTypes.push("Procesos Compartidos"); }
                        if (serviceTypes.length > 0) { x += addHtmlValue("Tipo", serviceTypes.join(', ')); }
                    }
                    if (service.startType) { x += addHtmlValue("Start Type", service.startType); }
                    if (service.user) { x += addHtmlValue("Usuario", service.user); }
                    if (service.installedBy) { x += addHtmlValue("Installed By", service.installedBy); }
                    if (service.installedDate) { x += addHtmlValue("Installed Date", printDateTime(new Date(service.installedDate))); }
                    if (service.failureActions) {
                        if (service.failureActions.resetPeriod) {
                            var abc = ((service.failureActions.resetPeriod < 86400 ? 0 : service.failureActions.resetPeriod) / (24 * 60 * 60));
                            x += addHtmlValue("Restart Fail Count After ", abc + ' Day' + (abc != 1 ? 's' : ''));
                        }
                        if (service.failureActions.actions) {
                            if (service.failureActions.actions[0]) { x += addHtmlValue("First Failure", service.failureActions.actions[0].type); }
                            if (service.failureActions.actions[1]) { x += addHtmlValue("Second Failure", service.failureActions.actions[1].type); }
                            if (service.failureActions.actions[2]) { x += addHtmlValue("Subsequent Failures", service.failureActions.actions[2].type); }
                        }
                    }
                    x += '<br/><div style=float:right;margin-bottom:12px><input type=button value="' + "Cerrar" + '" onclick=showServiceDetailsDialogEx(0,' + index + ')></div><div style=margin-bottom:12px><input type=button value="' + "Iniciar" + '" onclick=showServiceDetailsDialogEx(1,' + index + ')><input type=button value="' + "Detener" + '" onclick=showServiceDetailsDialogEx(2,' + index + ')><input type=button value="' + "Reiniciar" + '" onclick=showServiceDetailsDialogEx(3,' + index + ')></div>';
                    xxdialogTag = "service_" + index;
                    setModalContent('xxAddAgent', "Detalles del Servicio", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            }
        }

        function showServiceWaitDialog(index) {
            if (xxdialogMode) return false;
            var service = deskTools.services[index];
            if (service != null) {
                meshserver.send({ action: 'msg', type: 'service', nodeid: currentNode._id, serviceName: service.name });
                xxdialogTag = "service_" + index;
                setModalContent('xxAddAgent', "Detalles del Servicio", 'Requesting Service Details...');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
            return false;
        }

        function showServiceDetailsDialogEx(action, index) {
            setDialogMode(0);
            if (action == 0) return;
            var service = deskTools.services[index];
            if (service != null) {
                if (action == 1) { meshserver.send({ action: 'msg', type: 'serviceStart', nodeid: currentNode._id, serviceName: service.name }); }
                if (action == 2) { meshserver.send({ action: 'msg', type: 'serviceStop', nodeid: currentNode._id, serviceName: service.name }); }
                if (action == 3) { meshserver.send({ action: 'msg', type: 'serviceRestart', nodeid: currentNode._id, serviceName: service.name }); }
                setTimeout(function () { refreshDeskTools(1) }, 1000);
            }
        }

        // Toggle mouse and keyboard input
        function toggleKvmControl() { Q('DeskControl').blur(); putstore('DeskControl', (Q('DeskControl').checked ? 1 : 0)); QS('DeskControlSpan').color = Q('DeskControl').checked ? null : 'red'; }

        // Toggle desktop session recording
        function deskRecordSession() {
            if (desktop == null) return;
            if (desktop.m.recordedData == null) {
                // Start recording
                Q('DeskRecordButtonImage').style.color = 'red';
                desktop.m.StartRecording();
            } else {
                // Stop recording
                Q('DeskRecordButtonImage').style.color = '';
                var d = new Date(), n = "Sesión de Escritorio" + '-' + currentNode.name + '-' + d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2) + '-' + ('0' + d.getHours()).slice(-2) + '-' + ('0' + d.getMinutes()).slice(-2);
                saveAs(data2blob(desktop.m.StopRecording().join('')), n + '.mcrec');
            }
        }

        // Save the desktop image to file
        function deskSaveImage() {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            var d = new Date(), n = "Escritorio" + '-' + currentNode.name + '-' + d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2) + '-' + ('0' + d.getHours()).slice(-2) + '-' + ('0' + d.getMinutes()).slice(-2);
            Q('Desk')['toBlob'](function (blob) { saveAs(blob, n + '.png'); });
        }

        function deskDisplayInfo(sender, displays, selDisplay) {
            _deskLastDisplays = displays; _deskLastSelDisplay = selDisplay;
            var displayCount = 0, displaySelector = '';
            for (var i in displays) {
                displayCount++;
                var str = displays[i], allDisplays = 1;
                if (str == 'All Displays') { str = "Todas las Pantallas"; allDisplays = 2; } // Language translation
                if (str.startsWith('Display ')) { str = format("Mostrar {0}", str.substring(8)); } // Language translation
                var maintext = 'id=DeskMonitorSelectionX' + i + ' title="' + EscapeHtml(str) + '" onclick=deskSetDisplay(' + i + ') role=button'
                if (allDisplays == 2) {
                    displaySelector += '<span ' + maintext + ' class="fa-layers fa-fw ' + ((selDisplay == i) ? '' : ' gray') + '"><i class="fa-solid fa-desktop"></i><i class="fa-solid fa-square" data-fa-transform="shrink-12 left-3 up-3"></i><i class="fa-solid fa-square" data-fa-transform="shrink-12 right-3 up-3"></i></span>';
                } else {
                    displaySelector += '<i ' + maintext + ' class="fa-fw fa-solid fa-desktop ' + ((selDisplay == i) ? '' : ' gray') + '"></i>';
                }
                if ((deskPreferedStickyDisplay == i) && (selDisplay != deskPreferedStickyDisplay)) { desktop.m.SetDisplay(i); }
                deskPreferedStickyDisplay = -1;
            }
            QH('DeskMonitorSelectionSpan', displaySelector);
            QV('DeskMonitorSelectionSpan', displayCount > 1);
            QH('deskMobileMonitorIcons', displaySelector);
            QV('deskMobileMonitorRow', displayCount > 1);
            var panel = Q('deskMobileActionsPanel');
            if (panel && panel.style.display !== 'none' && document.body.classList.contains('fulldesk')) {
                _buildFullscreenPanel(panel);
            }
        }

        function deskGetDisplayNumbers(e) { desktop.m.GetDisplayNumbers(); }
        var deskPreferedStickyDisplay = -1;
        var _deskLastDisplays = null, _deskLastSelDisplay = null;
        function deskSetDisplay(v) {
            v = parseInt(v);
            desktop.m.SetDisplay(deskPreferedStickyDisplay = v);
            _deskLastSelDisplay = v;
            // Update selected state on monitor icons immediately (no server roundtrip for this)
            ['DeskMonitorSelectionSpan', 'deskMobileMonitorIcons'].forEach(function(id) {
                var container = Q(id);
                if (!container) return;
                container.querySelectorAll('[id^="DeskMonitorSelectionX"]').forEach(function(el) {
                    var idx = parseInt(el.id.replace('DeskMonitorSelectionX', ''));
                    if (idx === v) { el.classList.remove('gray'); } else { el.classList.add('gray'); }
                });
            });
            // Rebuild fullscreen panel if open so it reflects new selection
            var panel = Q('deskMobileActionsPanel');
            if (panel && panel.style.display !== 'none' && document.body.classList.contains('fulldesk')) {
                _buildFullscreenPanel(panel);
            }
        }

        // Double click detection. This is important for macOS.
        var dblClickDetectArgs = { t: 0, x: 0, y: 0 };
        function dblClickDetect(e) {
            if (e.buttons != 1) return;
            var t = Date.now();
            if (((t - dblClickDetectArgs.t) < 250) && (Math.abs(e.clientX - dblClickDetectArgs.x) < 2) && (Math.abs(e.clientY - dblClickDetectArgs.y) < 2)) {
                if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousedblclick(e); desktop.m.sendKeepAlive(); } else { desktop.m.mousedblclick(e); } }
            }
            dblClickDetectArgs.t = t;
            dblClickDetectArgs.x = e.clientX;
            dblClickDetectArgs.y = e.clientY;
        }

        function dmousedown(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousedown(e); desktop.m.sendKeepAlive(); } else { desktop.m.mousedown(e); } } dblClickDetect(e); var sk = Q('softKeyboard'); if (sk && !_mobileKbdEnabled && document.activeElement === sk) { sk.blur(); } }
        function dmouseup(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mouseup(e); desktop.m.sendKeepAlive(); } else { desktop.m.mouseup(e); } }
        function dmousemove(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { Q('Desk').style.cursor = ''; if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousemove(e); desktop.m.sendKeepAlive(); } else { desktop.m.mousemove(e); } } else if (!xxdialogMode && desktop != null && !Q('DeskControl').checked) { Q('Desk').style.cursor = 'not-allowed'; } }
        function dmousewheel(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousewheel(e); desktop.m.sendKeepAlive(); } else { if (desktop.m.mousewheel) { desktop.m.mousewheel(e); } } haltEvent(e); return true; } return false; }
        function drotate(x) { if (!xxdialogMode && desktop != null) { desktop.m.setRotation(desktop.m.rotation + x); deskAdjust(); deskAdjust(); } }

        // Mobile touch input for remote desktop.
        // 1-finger tap          = left click on remote
        // Direct mode  — 1-finger tap=click, long-press=right-click, drag=cursor move
        // Trackpad mode — 1-finger drag moves visible cursor; tap clicks at cursor position
        // Both modes    — 2-finger scroll = mouse wheel on remote
        var dtouchStartX = 0, dtouchStartY = 0, dtouchLastX = 0, dtouchLastY = 0;
        var dtouchMoved = false;
        var dtouchLastTapTime = 0, dtouchLastTapX = 0, dtouchLastTapY = 0; // double-tap tracking
        var dtouchLongPressTimer = null;
        var dtouchTwoFingerLastY = 0;
        var dtouchLongPressed = false; // long press fired; next move starts drag, lift sends right-click
        var dtouchDragActive  = false; // mousedown(left) sent; streaming drag until mouseup
        // Trackpad state (mirrors Classic Mobile)
        var trackpadMode      = false;
        var trackpadLastX     = null, trackpadLastY = null;
        var trackpadCurX      = 0, trackpadCurY  = 0;
        var trackpadDown      = false;
        var trackpadTapTimer  = null;
        var trackpadTapMoved  = false;
        var trackpadEdgeTimer = null;
        var trackpadEdgeDX    = 0, trackpadEdgeDY = 0;
        var trackpadLongPressed = false; // long press fired; next move starts drag, lift sends right-click
        var trackpadDragActive  = false; // mousedown(left) sent; streaming drag until mouseup

        function dtouchMakeEvent(clientX, clientY, button) {
            // When the canvas is rotated, crotX/crotY (agent-desktop-0.0.2.js) convert
            // canvas-pixel space -> remote-desktop space. We apply that correction here by
            // computing adjusted clientX/Y that will produce the correct remote coords after
            // SendMouseMsg's (pageX - offset) * scale calculation.
            var adjX = clientX, adjY = clientY;
            var dm = dtouchGetModule();
            if (dm && dm.rotation && dm.CanvasId && dm.Canvas && dm.crotX && dm.crotY) {
                var r = dm.CanvasId.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) {
                    var cw = dm.Canvas.canvas.width, ch = dm.Canvas.canvas.height;
                    var px = (clientX - r.left) * cw / r.width;
                    var py = (clientY - r.top)  * ch / r.height;
                    var rx = dm.crotX(px, py), ry = dm.crotY(px, py);
                    adjX = r.left + rx * r.width  / cw;
                    adjY = r.top  + ry * r.height / ch;
                }
            }
            return { pageX: adjX + window.scrollX, pageY: adjY + window.scrollY, button: button || 0, buttons: 0, which: (button === 2) ? 3 : 1 };
        }

        function dtouchGetModule() {
            return (webRtcDesktop && webRtcDesktop.softdesktop) ? webRtcDesktop.softdesktop.m : (desktop ? desktop.m : null);
        }

        // Trackpad mode (ported from Classic Mobile)
        function deskToggleTrackpad() {
            trackpadMode = !trackpadMode;
            if (trackpadMode) {
                var dp = Q('DeskParent'), r = dp.getBoundingClientRect();
                trackpadMoveCursor(r.left + r.width / 2, r.top + r.height / 2);
            } else {
                trackpadStopEdgeScroll();
                Q('trackpadCursor').style.display = 'none';
                trackpadCurX = 0; trackpadCurY = 0;
            }
        }

        function trackpadMoveCursor(clientX, clientY) {
            var deskRect = Q('Desk').getBoundingClientRect();
            // Clamp cursor within canvas bounds
            var cx = Math.max(0, Math.min(deskRect.width,  clientX - deskRect.left));
            var cy = Math.max(0, Math.min(deskRect.height, clientY - deskRect.top));
            var cursor = Q('trackpadCursor');
            // position:fixed -> coordinates are viewport-relative (never clipped by overflow:hidden)
            cursor.style.left = (deskRect.left + cx) + 'px';
            cursor.style.top  = (deskRect.top  + cy) + 'px';
            // Rotate cursor to match canvas rotation so it always points "up" in the current view
            var dm = dtouchGetModule();
            cursor.style.transform = 'rotate(' + ((dm ? (dm.rotation || 0) : 0) * 90) + 'deg)';
            cursor.style.display = 'block';
            trackpadCurX = cx; trackpadCurY = cy;
        }

        function trackpadMakeEvent(button) {
            // Build event at cursor position (not finger position); dtouchMakeEvent handles rotation
            var rect = Q('Desk').getBoundingClientRect();
            return dtouchMakeEvent(rect.left + trackpadCurX, rect.top + trackpadCurY, button);
        }

        function trackpadSendMove() {
            var dm = dtouchGetModule();
            if (!xxdialogMode && dm) dm.mousemove(trackpadMakeEvent(0));
        }

        function trackpadSendClick(button) {
            var dm = dtouchGetModule();
            if (!xxdialogMode && dm) { dm.mousedown(trackpadMakeEvent(button)); dm.mouseup(trackpadMakeEvent(button)); }
        }

        function trackpadStopEdgeScroll() {
            if (trackpadEdgeTimer) { clearInterval(trackpadEdgeTimer); trackpadEdgeTimer = null; }
            trackpadEdgeDX = 0; trackpadEdgeDY = 0;
        }

        function trackpadStartEdgeScroll() {
            if (trackpadEdgeTimer) return;
            trackpadEdgeTimer = setInterval(function() {
                if ((!trackpadMode && !dtouchDragActive) || (trackpadEdgeDX === 0 && trackpadEdgeDY === 0)) { trackpadStopEdgeScroll(); return; }
                var dp = Q('deskarea3x') || Q('DeskParent'); dp.scrollLeft += trackpadEdgeDX; dp.scrollTop += trackpadEdgeDY;
                if (dtouchDragActive) { var dm = dtouchGetModule(); if (dm && Q('DeskControl').checked) dm.mousemove(dtouchMakeEvent(dtouchLastX, dtouchLastY, 0)); }
            }, 16);
        }

        function trackpadCheckEdge(cx, cy) {
            if (cx === undefined) { var r = Q('trackpadCursor').getBoundingClientRect(); cx = r.left + 9; cy = r.top + 15; }
            var vpRect = (Q('deskarea3x') || Q('DeskParent')).getBoundingClientRect(), zone = 60;
            trackpadEdgeDX = cx < vpRect.left  + zone ? -Math.round((zone-(cx-vpRect.left))/4) :
                             cx > vpRect.right  - zone ?  Math.round((zone-(vpRect.right-cx))/4) : 0;
            trackpadEdgeDY = cy < vpRect.top   + zone ? -Math.round((zone-(cy-vpRect.top))/4) :
                             cy > vpRect.bottom - zone ?  Math.round((zone-(vpRect.bottom-cy))/4) : 0;
            if (trackpadEdgeDX || trackpadEdgeDY) trackpadStartEdgeScroll(); else trackpadStopEdgeScroll();
        }

        // Mobile sticky modifier keys (CTRL/SHIFT/ALT)
        var _mobileModifiers = { ctrl: false, shift: false, alt: false };
        var _modKeyCodes     = { ctrl: 17,    shift: 16,    alt: 18    };

        function _charToKeyCode(ch) {
            if (ch >= 97 && ch <= 122) return ch - 32; // a-z -> 65-90
            if (ch >= 65 && ch <= 90)  return ch;       // A-Z
            if (ch >= 48 && ch <= 57)  return ch;       // 0-9
            return 0;
        }

        function _updateMobileModifierUI() {
            var ids = { ctrl: 'deskModCtrl', shift: 'deskModShift', alt: 'deskModAlt' };
            for (var mod in ids) { var b = Q(ids[mod]); if (b) b.classList.toggle('armed', !!_mobileModifiers[mod]); }
        }

        function _releaseMobileModifiers() {
            var dm = desktop && desktop.m;
            for (var mod in _mobileModifiers) {
                if (_mobileModifiers[mod]) {
                    if (dm) dm.SendKeyMsgKC((mod === 'ctrl' || mod === 'alt') ? dm.KeyAction.EXUP : dm.KeyAction.UP, _modKeyCodes[mod]);
                    _mobileModifiers[mod] = false;
                }
            }
            _updateMobileModifierUI();
        }

        function toggleMobileModifier(mod) {
            if (!desktop || !desktop.m || !Q('DeskControl').checked) return;
            _mobileModifiers[mod] = !_mobileModifiers[mod];
            var dm = desktop.m, ext = (mod === 'ctrl' || mod === 'alt');
            dm.SendKeyMsgKC(_mobileModifiers[mod] ? (ext ? dm.KeyAction.EXDOWN : dm.KeyAction.DOWN) : (ext ? dm.KeyAction.EXUP : dm.KeyAction.UP), _modKeyCodes[mod]);
            _updateMobileModifierUI();
        }

        // Mobile soft-keyboard: focusing #softKeyboard shows the OS keyboard.
        // Key events bubble to document handlers (backspace, enter, etc.).
        // Mobile keyboards fire 'input' instead of keypress -> SendKeyUnicode.
        var mobileKbdGotKeypress = false;
        var _mobileKbdEnabled = false;

        function onSoftKeyboardInput(el) {
            if (!desktop || !desktop.m || !Q('DeskControl').checked) { el.value = ''; return; }
            if (mobileKbdGotKeypress) { el.value = ''; return; } // desktop keyboard — already handled by keypress
            var str = el.value; el.value = '';
            var anyMod = _mobileModifiers.ctrl || _mobileModifiers.shift || _mobileModifiers.alt;
            if (str && desktop.m.SendKeyUnicode) {
                for (var i = 0; i < str.length; i++) {
                    var c = str.charCodeAt(i);
                    if (anyMod && desktop.m.SendKeyMsgKC) {
                        var kc = _charToKeyCode(c);
                        if (kc) {
                            desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.DOWN, kc], [desktop.m.KeyAction.UP, kc]]);
                            continue;
                        }
                    }
                    desktop.m.SendKeyUnicode(desktop.m.KeyAction.DOWN, c);
                    desktop.m.SendKeyUnicode(desktop.m.KeyAction.UP, c);
                }
            }
            _releaseMobileModifiers();
        }

        function _updateSoftKbdIcon() {
            var btn = Q('DeskSoftKbdBtn');
            if (btn) {
                var icon = btn.querySelector('i');
                if (icon) icon.className = _mobileKbdEnabled ? 'fa-solid fa-keyboard text-warning' : 'fa-solid fa-keyboard';
            }
            var ki = Q('deskKbdItem');
            if (ki) {
                ki.innerHTML = '<i class="fa-solid fa-keyboard fa-fw me-2 text-primary"></i>' + "Teclado en pantalla"
                    + ' <i class="fa-solid ' + (_mobileKbdEnabled ? 'fa-toggle-on text-success' : 'fa-toggle-off text-secondary') + ' ms-1"></i>';
            }
        }

        function onSoftKeyboardFocusChange(focused) {
            _updateSoftKbdIcon();
        }

        function toggleMobileKeyboard() {
            var kb = Q('softKeyboard');
            if (!kb) return;
            _mobileKbdEnabled = !_mobileKbdEnabled;
            if (_mobileKbdEnabled) { kb.focus(); } else { kb.blur(); }
            _updateSoftKbdIcon();
        }

        function dtouchSendClick(clientX, clientY, button) {
            var ev = dtouchMakeEvent(clientX, clientY, button);
            var dm = dtouchGetModule();
            if (dm) { dm.mousedown(ev); dm.mouseup(ev); }
        }

        function dtouchstart(e) {
            setSessionActivity();
            deskCloseMobileActions();
            dtouchMoved = false;
            dtouchLongPressed = false; dtouchDragActive = false; trackpadStopEdgeScroll();
            if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
            if (xxdialogMode || desktop == null) return;
            e.preventDefault();
            // Trackpad mode
            if (trackpadMode) {
                trackpadStopEdgeScroll();
                trackpadLongPressed = false; trackpadDragActive = false;
                if (e.touches.length === 2) {
                    // Fall through to pinch-start recording below (same as direct mode)
                } else if (e.touches.length === 1) {
                    var t = e.touches[0];
                    trackpadLastX = t.clientX; trackpadLastY = t.clientY;
                    trackpadTapMoved = false;
                    trackpadTapTimer = setTimeout(function() { trackpadTapTimer = null; }, 250);
                    trackpadDown = false;
                    dtouchLongPressTimer = setTimeout(function() {
                        dtouchLongPressTimer = null;
                        if (trackpadTapMoved || xxdialogMode || desktop == null || !Q('DeskControl').checked) return;
                        trackpadLongPressed = true; // armed: next move -> drag, lift -> right-click
                        trackpadTapMoved = true;
                    }, 500);
                    return;
                } else { return; }
            }
            // Direct mode — 2 fingers
            if (e.touches.length === 2) {
                var fdx = e.touches[0].clientX - e.touches[1].clientX;
                var fdy = e.touches[0].clientY - e.touches[1].clientY;
                dtouchTwoFingerLastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
                if (_mobileZoomMode === 'native') {
                    // Record pinch start for zoom gesture
                    _nativePinchStartDist = Math.sqrt(fdx*fdx + fdy*fdy);
                    var sc = Q('deskarea3x');
                    // Use the CSS variable values as the authoritative current size so
                    // subsequent pinches start from the exact same dimension that was set.
                    var cssW = parseFloat(document.documentElement.style.getPropertyValue('--native-desk-w'));
                    var cssH = parseFloat(document.documentElement.style.getPropertyValue('--native-desk-h'));
                    _nativePinchStartW = (cssW > 0) ? cssW : (sc ? sc.scrollWidth  : 640);
                    _nativePinchStartH = (cssH > 0) ? cssH : (sc ? sc.scrollHeight : 480);
                    _nativePinchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
                    _nativePinchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
                }
                return;
            }
            if (e.touches.length !== 1) return;
            var t = e.touches[0];
            dtouchStartX = dtouchLastX = t.clientX;
            dtouchStartY = dtouchLastY = t.clientY;
            dtouchLongPressTimer = setTimeout(function() {
                dtouchLongPressTimer = null;
                if (dtouchMoved || xxdialogMode || desktop == null || !Q('DeskControl').checked) return;
                dtouchLongPressed = true; // armed: next move -> drag, lift -> right-click
                dtouchMoved = true;
            }, 500);
        }

        function dtouchmove(e) {
            setSessionActivity();
            if (xxdialogMode || desktop == null) return;
            e.preventDefault();
            // Trackpad mode
            if (trackpadMode) {
                if (e.touches.length === 1) {
                    var t = e.touches[0];
                    if (trackpadLastX === null) { trackpadLastX = t.clientX; trackpadLastY = t.clientY; return; }
                    var dx = t.clientX - trackpadLastX, dy = t.clientY - trackpadLastY;
                    trackpadLastX = t.clientX; trackpadLastY = t.clientY;
                    if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
                        trackpadTapMoved = true;
                        if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
                        // First move after long press -> start drag
                        if (trackpadLongPressed && !trackpadDragActive && Q('DeskControl').checked) {
                            trackpadDragActive = true;
                            var dm = dtouchGetModule(); if (dm) dm.mousedown(trackpadMakeEvent(0));
                        }
                    }
                    var rect = Q('Desk').getBoundingClientRect();
                    trackpadMoveCursor(rect.left + trackpadCurX + dx * 1.5, rect.top + trackpadCurY + dy * 1.5);
                    trackpadSendMove();
                    trackpadCheckEdge();
                    return;
                }
                // 2-finger pinch: fall through to native/direct zoom handling below
                if (e.touches.length !== 2) return;
                trackpadTapMoved = true; // prevent spurious tap on finger lift
            }
            // Native zoom mode — 1 finger: pan (skipped when drag gesture is active)
            if (_mobileZoomMode === 'native' && e.touches.length === 1 && !dtouchLongPressed && !dtouchDragActive) {
                var t = e.touches[0];
                var dx = t.clientX - dtouchLastX, dy = t.clientY - dtouchLastY;
                dtouchLastX = t.clientX; dtouchLastY = t.clientY;
                if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
                    dtouchMoved = true;
                    var sc = Q('deskarea3x');
                    if (sc) { sc.scrollLeft -= dx; sc.scrollTop -= dy; }
                }
                return;
            }
            // Native zoom mode — 2 fingers: pinch to resize canvas (zoom in/out)
            if (_mobileZoomMode === 'native' && e.touches.length === 2 && _nativePinchStartDist > 0) {
                var fdx = e.touches[0].clientX - e.touches[1].clientX;
                var fdy = e.touches[0].clientY - e.touches[1].clientY;
                var newDist = Math.sqrt(fdx*fdx + fdy*fdy);
                var ratio = newDist / _nativePinchStartDist;
                var newW = Math.round(_nativePinchStartW * ratio);
                var newH = Math.round(_nativePinchStartH * ratio);
                // Clamp between 25% and 400% of the native remote resolution
                var dm = dtouchGetModule();
                var minW = Math.round((dm ? dm.ScreenWidth  : 640) * 0.25);
                var maxW = Math.round((dm ? dm.ScreenWidth  : 640) * 4);
                var minH = Math.round((dm ? dm.ScreenHeight : 480) * 0.25);
                var maxH = Math.round((dm ? dm.ScreenHeight : 480) * 4);
                newW = Math.max(minW, Math.min(maxW, newW));
                newH = Math.max(minH, Math.min(maxH, newH));
                document.documentElement.style.setProperty('--native-desk-w', newW + 'px');
                document.documentElement.style.setProperty('--native-desk-h', newH + 'px');
                // Keep pinch midpoint fixed on screen by adjusting scroll
                var sc = Q('deskarea3x');
                if (sc) {
                    var scRect = sc.getBoundingClientRect();
                    var midViewX = _nativePinchMidX - scRect.left;
                    var midViewY = _nativePinchMidY - scRect.top;
                    sc.scrollLeft = (sc.scrollLeft + midViewX) * ratio - midViewX;
                    sc.scrollTop  = (sc.scrollTop  + midViewY) * ratio - midViewY;
                    // Update start values for incremental updates each frame
                    _nativePinchStartDist = newDist;
                    _nativePinchStartW = newW;
                    _nativePinchStartH = newH;
                }
                dtouchMoved = true;
                return;
            }
            // Direct mode
            if (e.touches.length === 2) {
                var midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
                var midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
                var dy = dtouchTwoFingerLastY - midY;
                dtouchTwoFingerLastY = midY;
                if (Math.abs(dy) > 1 && Q('DeskControl').checked) {
                    var dm = dtouchGetModule();
                    if (dm && dm.SendMouseMsg && dm.KeyAction) {
                        // Use last known cursor position so scroll doesn't move the remote cursor
                        var _scrollEv = trackpadMode ? trackpadMakeEvent(0) : dtouchMakeEvent(dtouchLastX, dtouchLastY, 0);
                        _scrollEv.wheelDelta = -dy * 4;
                        dm.SendMouseMsg(dm.KeyAction.SCROLL, _scrollEv);
                    }
                }
                return;
            }
            if (e.touches.length !== 1) return;
            var t = e.touches[0];
            dtouchLastX = t.clientX; dtouchLastY = t.clientY;
            // Stream active drag
            if (dtouchDragActive) {
                if (Q('DeskControl').checked) { var dm = dtouchGetModule(); if (dm) dm.mousemove(dtouchMakeEvent(t.clientX, t.clientY, 0)); }
                trackpadCheckEdge(t.clientX, t.clientY);
                return;
            }
            // Finger moved after long press -> start drag
            if (dtouchLongPressed) {
                if (Math.abs(t.clientX - dtouchStartX) > 4 || Math.abs(t.clientY - dtouchStartY) > 4) {
                    if (Q('DeskControl').checked) {
                        dtouchDragActive = true;
                        var dm = dtouchGetModule();
                        if (dm) { dm.mousedown(dtouchMakeEvent(dtouchStartX, dtouchStartY, 0)); dm.mousemove(dtouchMakeEvent(t.clientX, t.clientY, 0)); }
                    } else { dtouchLongPressed = false; }
                }
                return;
            }
            // Normal cursor move
            if (Math.abs(t.clientX - dtouchStartX) > 8 || Math.abs(t.clientY - dtouchStartY) > 8) {
                dtouchMoved = true;
                if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
                if (Q('DeskControl').checked) { var dm = dtouchGetModule(); if (dm) dm.mousemove(dtouchMakeEvent(t.clientX, t.clientY, 0)); }
            }
        }

        function dtouchend(e) {
            setSessionActivity();
            if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
            if (xxdialogMode || desktop == null) return;
            e.preventDefault();
            // Trackpad mode
            if (trackpadMode) {
                trackpadStopEdgeScroll();
                trackpadLastX = null; trackpadLastY = null; trackpadDown = false;
                if (trackpadDragActive) {
                    trackpadDragActive = false; trackpadLongPressed = false;
                    if (Q('DeskControl').checked) { var dm = dtouchGetModule(); if (dm) dm.mouseup(trackpadMakeEvent(0)); }
                    return;
                }
                if (trackpadLongPressed) {
                    trackpadLongPressed = false;
                    if (Q('DeskControl').checked) trackpadSendClick(2);
                    return;
                }
                if (trackpadTapTimer !== null && !trackpadTapMoved) {
                    clearTimeout(trackpadTapTimer); trackpadTapTimer = null;
                    if (Q('DeskControl').checked) trackpadSendClick(0);
                }
                return;
            }
            // Direct mode
            if (!Q('DeskControl').checked) return;
            if (e.changedTouches.length !== 1) return;
            var t = e.changedTouches[0];
            // Drag end: send mouseup at final finger position
            if (dtouchDragActive) {
                dtouchDragActive = false; dtouchLongPressed = false;
                trackpadStopEdgeScroll();
                var dm = dtouchGetModule(); if (dm) dm.mouseup(dtouchMakeEvent(t.clientX, t.clientY, 0));
                return;
            }
            // Long press without drag -> right-click
            if (dtouchLongPressed) {
                dtouchLongPressed = false;
                dtouchSendClick(dtouchLastX, dtouchLastY, 2);
                return;
            }
            // 30px threshold (not dtouchMoved's 8px) — finger pressure causes micro-movement
            // that would block taps; only real drags exceed 30px.
            var totalDx = Math.abs(t.clientX - dtouchStartX);
            var totalDy = Math.abs(t.clientY - dtouchStartY);
            if (totalDx > 30 || totalDy > 30) return; // real drag — no click
            var now = Date.now();
            var dx = Math.abs(t.clientX - dtouchLastTapX), dy = Math.abs(t.clientY - dtouchLastTapY);
            if ((now - dtouchLastTapTime) < 400 && dx < 30 && dy < 30) {
                // Use first tap's position: Windows requires both clicks within 4 remote px;
                // a 2px finger shift × ~5x scale = 10 remote px, breaking detection.
                dtouchSendClick(dtouchLastTapX, dtouchLastTapY, 0);
                dtouchLastTapTime = 0; // reset so a 3rd tap doesn't re-trigger
            } else {
                dtouchSendClick(t.clientX, t.clientY, 0);
                dtouchLastTapTime = now;
                dtouchLastTapX = t.clientX;
                dtouchLastTapY = t.clientY;
            }
        }
        function stopProcess(id, name) {
            setModalContent('xxAddAgent', "Control de Proceso", format("Detener proceso #{0} \"{1}\"?", id, name));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => stopProcessEx(3, (id + '|' + name)));
            return false;
        }
        function stopProcessEx(buttons, tag) { meshserver.send({ action: 'msg', type: 'pskill', nodeid: currentNode._id, value: tag }); setTimeout(refreshDeskTools, 300); }

        //
        // TERMINAL
        //

        var terminalNode;
        function setupTerminal() {
            // Setup the terminal
            if ((terminalNode != currentNode) && (terminal != null)) {
                if (terminalNode._id != currentNode._id) {
                    terminal.Stop(); terminalNode = null; terminal = null;
                }
            }
            terminalNode = currentNode;
            updateTerminalButtons();
        }

        // Show and enable the right buttons
        function updateTerminalButtons() {
            var termState = ((terminal != null) && (terminal.state != 0));

            // If we are looking at a local non-windows device, enable terminal capability.
            if ((terminalNode.mtype == 3) && (terminalNode.agent != null) && (terminalNode.agent.id > 4) && (features2 & 0x00000200)) { terminalNode.agent.caps = 6; } // 1 = Terminal, 2 = Desktop, 4 = files

            // Show the right buttons
            QV('disconnectbutton2span', (termState == true));
            QV('connectbutton2span', (termState == false) && (terminalNode.agent != null) && (terminalNode.agent.caps & 2) && (terminalNode.mtype != 3));
            QV('connectbutton2sspan', (features2 & 0x00000200) && (termState == false) && (terminalNode.agent != null) && (terminalNode.agent.caps & 2) && (terminalNode.agent.id != 3) && ((features2 & 0x1000000) == 0));
            if (terminalNode.mtype == 1) {
                QV('connectbutton2hspan', (termState == false) && (terminalNode.intelamt != null) && (terminalNode.intelamt.state == 2));
                QV('terminalSizeDropDown', (termState == false) && (terminalNode.intelamt != null) && (terminalNode.intelamt.state == 2));
            } else {
                QV('connectbutton2hspan', (termState == false) && (terminalNode.intelamt != null) && (terminalNode.intelamt.state == 2) && (terminalNode.intelamt.ver != null));
                QV('terminalSizeDropDown', (termState == false) && (terminalNode.intelamt != null) && (terminalNode.intelamt.state == 2) && (terminalNode.intelamt.ver != null));
            }

            // Enable action button if mesh type is not "local devices"
            QV('termActionsBtn', terminalNode.mtype != 3);
            if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent == null) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
                QH('terminalCustomUpperRight', '');
            } else {
                QH('terminalCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("Puerto SSH {0}", (terminalNode.sshport ? terminalNode.sshport : 22)) + '</a>');
            }

            // Enable buttons
            var online = ((terminalNode.conn & 1) != 0) || (terminalNode.mtype == 3); // If Agent (1) connected, enable Terminal
            QE('connectbutton2', online); QE('connectbutton2d', online); // Terminal Connect and dropdown
            QE('connectbutton2s', online); QE('connectbutton2sd', online); // Terminal SSH Connect and dropdown
            var hwonline = ((terminalNode.conn & 6) != 0); // If CIRA (2) or AMT (4) connected, enable hardware terminal
            QE('connectbutton2h', hwonline);

            // Key buttons
            QE('ctrlcbutton', termState);
            QE('ctrlxbutton', termState);
            QE('escbutton', termState);
            QE('bsbutton', termState);
            QE('pastebutton', termState);
            QE('specialkeylist', termState);
            QE('specialkeylistinput', termState);

            // Terminal settings
            QV('terminalSettingsButtons', (terminal) && (terminal.contype == 2));
            if (terminal) {
                Q('id_ttypebutton').value = terminalEmulations[terminal.m.terminalEmulation];
                Q('id_tfxkeysbutton').value = fxEmulations[terminal.m.fxEmulation];
                Q('id_tcrbutton').value = (terminal.m.lineFeed == '\r\n') ? "CR+LF" : "LF";
            }

            // Display extra buttons on legacy terminal
            var xtermActive = !((args.xterm === 0) || ((terminal != null) && (xterm == null)));
            QV('termarea3xdiv', xtermActive);
            QV('Term', !xtermActive);
            QV('bsbutton', !xtermActive);
            QV('pastebutton', !xtermActive);
            QV('devListToolbarViewIcons2', xtermActive);
            QE('termSizeList', terminal == null);
        }

        // Called when the terminal state changes
        function onTerminalStateChange(xterminal, state) {
            if (terminal != xterminal) return;
            var xstate = state;
            if ((xstate == 3) && (xterminal.contype == 2)) { xstate++; }
            var str = StatusStrs[xstate];
            if (xstate == 3) {
                if (xterminal.contype == 3) { str += ", SSH"; }
                if (xterminal.webRtcActive == true) { str += ", WebRTC"; }
            }
            QH('termstatus', str);
            switch (state) {
                case 0:
                    // Disconnected, clear the terminal
                    QH('termtitle', '');
                    QV('termRecordIcon', false);
                    if (xterm == null) {
                        try { xterminal.m.TermResetScreen(); xterminal.m.TermDraw(); } catch (ex) { }
                    } else {
                        xterm.dispose();
                        xterm = xtermfit = xtermimage = null;
                    }
                    if (xterminal != null) { xterminal.Stop(); terminal = null; }
                    break;
                case 3:
                    if (xterminal && (xterminal.serverIsRecording == true)) { QV('termRecordIcon', true); }
                    xterminal.startTime = new Date();
                    if (updateSessionTimer == null) { updateSessionTimer = setInterval(updateSessionTime, 1000); }
                    if (xterm != null) { xterm.focus(); }
                    break;
                default:
                    //console.log('Unhandled onTerminalStateChange state', state);
                    break;
            }
            updateTerminalButtons();
        }

        // DEBUG
        var autoConnectTerminalTimer = null;
        function autoConnectTerminal(e) { if (autoConnectTerminalTimer == null) { autoConnectTerminalTimer = setInterval(connectTerminal, 100); } else { clearInterval(autoConnectTerminalTimer); autoConnectTerminalTimer = null; } }

        // Handles a tunnel to a remote shell
        function CreateRemoteTunnel(onTunnelUpdate, options) {
            var obj = { protocol: 1 };
            if ((options != null) && (typeof options.protocol == 'number')) { obj.protocol = options.protocol; }
            obj.onTunnelUpdate = onTunnelUpdate;
            obj.xxStateChange = function (state) { }
            obj.ProcessBinaryData = function (data) { obj.onTunnelUpdate(data); }
            obj.ProcessData = function (data) { obj.onTunnelUpdate(data); }
            obj.terminalEmulation = 1;
            obj.fxEmulation = 0;
            obj.lineFeed = '\r\n';
            return obj;
        }

        function tunnelUpdate(data) {
            if (xterm != null) {
                if (xterm.writeUtf8) {
                    if (typeof data == 'string') { xterm.writeUtf8(data); } else { xterm.writeUtf8(new Uint8Array(data)); }
                } else {
                    if (typeof data == 'string') { xterm.write(data); } else { xterm.write(new Uint8Array(data)); }
                }
            }
        }

        function sshTunnelAuthDialog(j, func) {
            var x = '';
            if (j.askkeypass) {
                x += addHtmlFormFloating("Autenticación", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=3 selected>' + "Clave almacenada" + '</option><option value=1>' + "Usuario Contraseña" + '</option><option value=2>' + "Nombre de usuario y clave" + '</option></select>');
            } else {
                x += addHtmlFormFloating("Autenticación", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=1 selected>' + "Usuario Contraseña" + '</option><option value=2>' + "Nombre de usuario y clave" + '</option></select>');
            }
            x += '<div id=d2userauth style=display:none>';
            x += addHtmlFormFloating("Nombre de Usuario", '<input id=dp2user class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
            x += '</div>';
            x += '<div id=d2passauth style=display:none>';
            x += addHtmlFormFloating("Contraseña", '<input type=password id=dp2pass class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
            if ((features2 & 0x00400000) == 0) { x += addHtmlValue('', '<label><input id=dp2keep type=checkbox class="form-check-input me-2">' + "Recuerda credenciales" + '</label>'); }
            x += '</div><div id=d2keyauth style=display:none>';
            x += addHtmlValue("Archivo de clave", '<input type=file id=dp2key class="form-control" maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />' + '<div id=d2badkey style=font-size:x-small>' + "El archivo de clave debe estar en formato OpenSSH." + '</div>');
            x += addHtmlFormFloating("Contraseña clave", '<input type=password id=dp2keypass class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
            if ((features2 & 0x00400000) == 0) {
                x += addHtmlValue('', '<label><input id=dp2keep1 type=checkbox class="form-check-input me-2" onchange=sshAuthUpdate(event)>' + "Recordar usuario y clave" + '</label>');
                x += addHtmlValue('', '<label><input id=dp2keep2 type=checkbox class="form-check-input me-2">' + "Recordar contraseña" + '</label>');
            }
            x += '</div>';
            if (j.askkeypass) {
                x += '<div id=d2keyauth2 style=display:none>';
                x += addHtmlFormFloating("Contraseña", '<input type=password id=dp2keypass2 class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
                x += '</div>';
            }
            setModalContent('xxAddAgent', "Autenticación", x);
            var closeFunc = function(event,a) {
                document.getElementById('xxAddAgentModal').removeEventListener('hide.bs.modal', closeFunc);
                connectTerminal();
            }
            document.getElementById('xxAddAgentModal').addEventListener('hide.bs.modal', closeFunc);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => {
                document.getElementById('xxAddAgentModal').removeEventListener('hide.bs.modal', closeFunc);
                func(11, 'ssh');
            });
            Q('dp2user').focus();
            sshAuthUpdate();
            setTimeout(sshAuthUpdate, 50);
        }

        function sshTunnelUpdate(data) {
            if (typeof data == 'string') {
                if (data[0] == '{') {
                    var j = JSON.parse(data);
                    switch (j.action) {
                        case 'sshauth': {
                            sshTunnelAuthDialog(j, sshConnectEx);
                            break;
                        }
                        case 'sshautoauth': {
                            terminal.socket.send(JSON.stringify({ action: 'sshautoauth', cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
                            break;
                        }
                        case 'connectionerror': { p12setConsoleMsg("Error de Conexión", 5000); break; }
                        case 'autherror': { p12setConsoleMsg("Error de Autenticación", 5000); break; }
                        case 'sessionerror': { p12setConsoleMsg("Sesión expirada", 5000); break; }
                        case 'sessiontimeout': { p12setConsoleMsg("Hora de término de la sesión", 5000); break; }
                    }
                } else if (data[0] == '~') {
                    if (xterm.writeUtf8) { xterm.writeUtf8(data.substring(1)); } else { xterm.write(data.substring(1)); }
                }
            }
        }

        function sshAuthUpdate(e) {
            QV('d2userauth', Q('dp2authmethod').value != 3);
            QV('d2passauth', Q('dp2authmethod').value == 1);
            QV('d2keyauth', Q('dp2authmethod').value == 2);
            QV('d2keyauth2', Q('dp2authmethod').value == 3);
            if (Q('dp2authmethod').value == 1) {
                QE('idx_dlgOkButton', (Q('dp2user').value.length > 0) && (Q('dp2pass').value.length > 0));
            } else if (Q('dp2authmethod').value == 3) {
                QE('idx_dlgOkButton', Q('dp2keypass2').value.length > 0);
            } else {
                QE('idx_dlgOkButton', false);
                if ((features2 & 0x00400000) == 0) { QE('dp2keep2', Q('dp2keep1').checked); }
                var ok = (Q('dp2user').value.length > 0) && (Q('dp2key').files != null) && (Q('dp2key').files.length == 1) && (Q('dp2key').files[0].size < 8000);
                if (ok == true) {
                    var reader = new FileReader();
                    reader.onload = function (e) {
                        var validkey =
                            ((e.target.result.indexOf('-----BEGIN OPENSSH PRIVATE KEY-----') >= 0) && (e.target.result.indexOf('-----END OPENSSH PRIVATE KEY-----') >= 0)) ||
                            ((e.target.result.indexOf('-----BEGIN RSA PRIVATE KEY-----') >= 0) && (e.target.result.indexOf('-----END RSA PRIVATE KEY-----') >= 0));
                        QE('idx_dlgOkButton', validkey);
                        QS('d2badkey')['color'] = validkey ? '#000' : '#F00';
                    }
                    reader.readAsText(Q('dp2key').files[0]);
                }
            }

            // When the enter key is pressed, move to the next field
            if (e && (e.keyCode == 13) && (e.target) && (Q('dp2authmethod').value == 1)) {
                if (e.target.id == 'dp2user') { Q('dp2pass').focus(); }
                if (e.target.id == 'dp2pass') { dialogclose(1); }
            }
        }

        function sshConnectEx(b) {
            if (b == 0) {
                if (terminal != null) { connectTerminal(); } // Disconnect
            } else {
                var keep = 0;
                if (Q('dp2authmethod').value == 1) {
                    if ((features2 & 0x00400000) == 0) { keep = (Q('dp2keep').checked ? 1 : 0); }
                    terminal.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: keep, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
                } else if (Q('dp2authmethod').value == 3) {
                    terminal.socket.send(JSON.stringify({ action: 'sshkeyauth', keypass: Q('dp2keypass2').value, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
                } else {
                    if ((features2 & 0x00400000) == 0) { keep = (Q('dp2keep1').checked ? 1 : 0); if (keep == 1) { keep += (Q('dp2keep2').checked ? 1 : 0); } } // Keep: 1 = user & key, 2 = User, key and password
                    var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value;
                    reader.onload = function (e) { terminal.socket.send(JSON.stringify({ action: 'sshauth', username: username, keypass: keypass, key: e.target.result, keep: keep, cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight })); }
                    reader.readAsText(Q('dp2key').files[0]);
                }
            }
        }

        // Send the new terminal size to the agent
        function xTermSendResize() {
            xtermResizeTimer = null;
            if ((xterm != null) && (terminal != null) && (terminal.sendCtrlMsg != null)) {
                if (terminal.urlname == 'sshterminalrelay.ashx') {
                    terminal.socket.send(JSON.stringify({ action: 'resize', cols: xterm.cols, rows: xterm.rows, width: Q('termarea3xdiv').offsetWidth, height: Q('termarea3xdiv').offsetHeight }));
                } else {
                    terminal.sendCtrlMsg(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: xterm.cols, rows: xterm.rows }));
                }
            }
        }

        // contype: 1 = Agent, 2 = AMT, 3 = SSH
        function connectTerminal(e, contype, options) {
            p12clearConsoleMsg();
            if (!terminal) {
                if (contype == 2) {
                    // Setup the Intel AMT terminal
                    if ((terminalNode.intelamt.user == null) || (terminalNode.intelamt.user == '')) { editDeviceAmtSettings(terminalNode._id, connectTerminal, 2); return; }
                    var termoptions = {};
                    if (Q('termSizeList').value == 1) { termoptions.cols = 80; termoptions.rows = 25; }
                    else if (Q('termSizeList').value == 2) { termoptions.cols = 100; termoptions.rows = 30; }

                    // Get out of full screen
                    if (fullscreen) { deskToggleFull(); }

                    // Setup legacy terminal
                    QV('termarea3xdiv', false);
                    QV('Term', true);
                    terminal = CreateAmtRedirect(CreateAmtRemoteTerminal('Term', termoptions), authCookie);
                    terminal.debugmode = debugmode;
                    terminal.m.debugmode = debugmode;
                    terminal.m.onTitleChange = function (sender, title) { QH('termtitle', ' - ' + EscapeHtml(title)); }
                    terminal.onStateChanged = onTerminalStateChange;
                    terminal.Start(terminalNode._id, 16994, '*', '*', 0);
                    terminal.contype = 2;
                    Q('id_ttypebutton').value = terminalEmulations[terminal.m.terminalEmulation];
                } else {
                    // Terminal setup
                    var termoptions = { protocol: ((options != null) && (typeof options.protocol == 'number')) ? options.protocol : 1 };
                    if (options && options.requireLogin) { termoptions.requireLogin = true; }
                    if (options && options.consent) { termoptions.consent = options.consent; }
                    if ([1, 2, 3, 4, 21, 22].indexOf(currentNode.agent.id) == -1) {
                        if (Q('termSizeList').value == 1) { termoptions.cols = 80; termoptions.rows = 25; termoptions.xterm = true; }
                        else if (Q('termSizeList').value == 2) { termoptions.cols = 100; termoptions.rows = 30; termoptions.xterm = true; }
                        else if (Q('termSizeList').value == 3) {
                            // TODO: Try to improve terminal auto-size.
                            termoptions.cols = Math.floor((Q('column_l').clientWidth - 60) / 10);
                            termoptions.rows = Math.floor((Q('column_l').clientHeight - 120) / 20);
                            termoptions.xterm = true;
                        }
                    }

                    // If shift is pressed
                    if ((e && (e.shiftKey == true))) {
                        if (currentNode.agent.id > 4) {
                            if (termoptions.protocol == 1) { termoptions.protocol = 7; } // Switch to user shell
                        } else {
                            if (termoptions.protocol == 1) { termoptions.protocol = 6; } // Switch to Powershell
                        }
                    }

                    // If the server requires a shell type
                    if ((serverinfo.linuxshell) != null && (currentNode.agent.id > 4)) {
                        if (serverinfo.linuxshell == 'root') { termoptions.protocol = 1; delete termoptions.requireLogin; }
                        if (serverinfo.linuxshell == 'user') { termoptions.protocol = 8; delete termoptions.requireLogin; }
                        if (serverinfo.linuxshell == 'login') { termoptions.protocol = 1; termoptions.requireLogin = true; }
                    }

                    if (args.xterm !== 0) {
                        // Setup a mesh agent xterm terminal
                        QV('termarea3xdiv', true);
                        QV('Term', false);

                        // Setup the terminal with auto-fit
                        if (xterm != null) { xterm.dispose(); }
                        xtermfit = new FitAddon.FitAddon();
                        xtermimage = new ImageAddon.ImageAddon();
                        xterm = new Terminal({ allowProposedApi: true });
                        if (xtermfit) { xterm.loadAddon(xtermfit); }
                        xterm.loadAddon(xtermimage);
                        xterm.open(Q('termarea3xdiv')); // termarea3x
                        xterm.onData(function (data) { if (terminal != null) { if (terminal.urlname == 'sshterminalrelay.ashx') { terminal.socket.send('~' + data); } else { terminal.sendText(data); } } })
                        if (document.body.classList.contains('is-mobile')) {
                            Q('termarea3xdiv').addEventListener('touchend', function() { xterm.focus(); });
                        }
                        if (xtermfit) { xtermfit.fit(); }
                        xterm.onTitleChange(function (title) { QH('termtitle', ' - ' + EscapeHtml(title)); });
                        xterm.onResize(function (size) {
                            // Despam resize
                            if (xtermResizeTimer) clearTimeout(xtermResizeTimer);
                            xtermResizeTimer = setTimeout(xTermSendResize, 200);
                        });

                        // Setup a terminal tunnel to the agent
                        termoptions.cols = xterm.cols;
                        termoptions.rows = xterm.rows;
                        terminal = CreateAgentRedirect(meshserver, CreateRemoteTunnel((contype == 3) ? sshTunnelUpdate : tunnelUpdate, termoptions), serverPublicNamePort, authCookie, authRelayCookie, domainUrl);
                        if (contype == 3) { terminal.urlname = 'sshterminalrelay.ashx'; } // If this is a SSH session, change the URL to the SSH application relay.
                        terminal.debugmode = debugmode;
                        terminal.m.debugmode = debugmode;
                        terminal.options = termoptions;
                        if (termoptions.requireLogin) { terminal.options.requireLogin = true; }
                        terminal.Start(terminalNode._id);
                        terminal.onStateChanged = onTerminalStateChange;
                        terminal.contype = contype;
                        terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it.
                        terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); }
                    } else {
                        QV('termarea3xdiv', false);
                        QV('Term', true);

                        // Setup a mesh agent legacy terminal
                        terminal = CreateAgentRedirect(meshserver, CreateAmtRemoteTerminal('Term', termoptions), serverPublicNamePort, authCookie, authRelayCookie, domainUrl);
                        terminal.options = termoptions;
                        terminal.debugmode = debugmode;
                        terminal.m.debugmode = debugmode;
                        terminal.m.onTitleChange = function (sender, title) { QH('termtitle', ' - ' + EscapeHtml(title)); }
                        terminal.m.lineFeed = ([1, 2, 3, 4, 21, 22].indexOf(currentNode.agent.id) >= 0) ? '\r\n' : '\r'; // On windows, send \r\n, on Linux only \r
                        terminal.attemptWebRTC = false; // Never do WebRTC on terminal, because of a race condition we can't do it.
                        terminal.onStateChanged = onTerminalStateChange;
                        terminal.onConsoleMessageChange = function () { p12setConsoleMsg(terminal.consoleMessage ? formatAgentConsoleMessage(terminal.consoleMessage, terminal.consoleMessageId, terminal.consoleMessageArgs) : null, terminal.consoleMessageTimeout); }
                        terminal.Start(terminalNode._id);
                        terminal.contype = contype;
                        terminal.m.terminalEmulation = 0;
                        terminal.m.fxEmulation = 0;
                        Q('id_ttypebutton').value = terminalEmulations[0];
                    }
                }
            } else {
                //QH('Term', '');
                if (xxdialogTag == 'ssh') { setDialogMode(0); }
                terminal.Stop();
                terminal = null;
                if (fullscreen) { deskToggleFull(); }
            }
            Q('connectbutton2').blur(); // Deselect the connect button so the button does not get key presses.
        }

        var terminalEmulations = ["Terminal UTF8", "ASCII extendido", "Intel ASCII"];
        function termToggleType() {
            if (!terminal || xxdialogMode) return;
            terminal.m.terminalEmulation = (terminal.m.terminalEmulation + 1) % 3;
            Q('id_ttypebutton').value = terminalEmulations[terminal.m.terminalEmulation];
            Q('id_ttypebutton').blur(); // Deselect the connect button so the button does not get key presses.
        }

        var fxEmulations = ["Intel (F10 = ESC+[OM)", "Alternar (F10 = ESC+0)", "VT100+ (F10 = ESC+[OY)"];
        function termToggleFx() {
            if (!terminal || xxdialogMode) return;
            terminal.m.fxEmulation = (terminal.m.fxEmulation + 1) % 3;
            Q('id_tfxkeysbutton').value = fxEmulations[terminal.m.fxEmulation];
            Q('id_tfxkeysbutton').blur(); // Deselect the connect button so the button does not get key presses.
        }

        function termToggleCr() {
            if (!terminal || xxdialogMode) return;
            if (terminal.m.lineFeed == '\n') { terminal.m.lineFeed = '\r\n'; } else { terminal.m.lineFeed = '\n'; }
            Q('id_tcrbutton').value = (terminal.m.lineFeed == '\r\n') ? "CR+LF" : "LF";
        }

        function termSendKey(key, id) {
            if (!terminal || xxdialogMode) return;
            if (xterm != null) {
                if (terminal.urlname == 'sshterminalrelay.ashx') {
                    // SSH
                    terminal.socket.send('~' + String.fromCharCode(key));
                } else if (terminal.sendText) {
                    // MeshAgent
                    terminal.sendText(String.fromCharCode(key));
                } else {
                    // CIRA
                    terminal.send(String.fromCharCode(key));
                }
                xterm.focus();
            } else if (terminal != null) {
                terminal.m.TermSendKey(key);
                Q(id).blur(); // Deselect the connect button so the button does not get key presses.
            }
        }

        function showTermPasteDialog() {
            if (!terminal || xxdialogMode) return;
            Q('pastebutton').blur();
            setModalContent('xxAddAgent', "Pegar", '<textarea id=d2pasteText style="width:100%;height:184px;resize:none"></textarea>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showTermPasteDialogEx());
            Q('d2pasteText').focus();
        }

        function showTermPasteDialogEx() {
            if (!terminal) return;
            terminal.m.TermSendKeys(Q('d2pasteText').value);
        }

        // Send special key
        function sendSpecialKey() {
            if (xterm != null) {
                if (terminal.urlname == 'sshterminalrelay.ashx') {
                    // SSH
                    terminal.socket.send('~' + String.fromCharCode(Q('specialkeylist').value));
                } else {
                    // Agent terminal
                    terminal.sendText(String.fromCharCode(Q('specialkeylist').value));
                }
                xterm.focus();
            } else if (terminal != null) {
                // Legacy terminal
                terminal.m.TermSendKey(Q('specialkeylist').value);
                Q('specialkeylist').blur();
                Q('specialkeylistinput').blur();
            }
        }

        //
        // FILES
        //

        var filesNode;
        function setupFiles() {
            // Setup the files tab
            if ((filesNode != currentNode) && (files != null)) {
                if (filesNode._id != currentNode._id) {
                    files.Stop(); filesNode = null; files = null;
                }
            }
            var samenode = (filesNode == currentNode);
            filesNode = currentNode;
            var online = ((filesNode.conn & 1) != 0) || (filesNode.mtype == 3); // If Agent (1) connected, enable Terminal
            QE('p13Connect', online); QE('p13Connectdrop', online); // Files Connect button and dropdown
            QE('p13Connects', online); QE('p13Connectsdrop', online); // Files SFTP Connect button and dropdown
            QV('p13Connectspan', (files == null) && (filesNode.mtype == 2));
            QV('p13Connectsspan', ((features2 & 0x200) != 0) && (filesNode.agent != null) && (filesNode.agent.id != 3) && (filesNode.agent.id != 4) && ((features2 & 0x800000) == 0));
            QV('p13Disconnect', files != null);
            p13setActions();
        }

        function onFilesStateChange(xfiles, state) {
            setSessionActivity();
            QV('p13Connectspan', (state == 0) && (filesNode.mtype == 2)); // Files Connect button and dropdown
            QV('p13Connectsspan', (state == 0) && (features2 & 0x200) && (filesNode.agent != null) && (filesNode.agent.id != 3) && (filesNode.agent.id != 4) && ((features2 & 0x800000) == 0));  // Files SFTP Connect button and dropdown
            QV('p13Disconnect', state != 0);
            QV('p13pathrow', state == 3);
            if (state != 3) { QV('p13currentpathinput', false); QV('p13currentpath', true); }
            var str = StatusStrs[state];
            if (state == 3) {
                if (files.contype == 2) { str += ", SFTP"; }
                if (files.webRtcActive == true) { str += ", WebRTC"; }
            }
            Q('p13Status').textContent = str;
            switch (state) {
                case 0:
                    // Disconnected, clear the files
                    QH('p13files', '');
                    p13filetree = null;
                    p13filetreelocation = [];
                    QH('p13currentpath', '');
                    Q('p13currentpathinput').value = '';
                    QV('p13currentpathinput', false);
                    QV('p13currentpath', true);
                    QE('p13FolderUp', false);
                    QV('filesRecordIcon', false);
                    p13setActions();
                    if (files != null) { files.Stop(); files = null; }
                    if (xxdialogTag == 'fileMsgDialog') { setDialogMode(0); }
                    if (uploadFile != null) { p13uploadFileTransferDone(); uploadFile = null; }
                    break;
                case 3:
                    p13filetreelocation = [];
                    p13targetpath = '';
                    if (files) {
                        var filepaths = [];
                        try { filepaths = JSON.parse(getstore('_devFilePaths', '[]')); } catch (ex) { }
                        for (var i = 0; i < filepaths.length; i++) { if (filepaths[i].n == currentNode._id) { p13targetpath = filepaths[i].p; } }
                        p13filetreelocation = p13targetpath.split('/');
                        files.sendText({ action: 'ls', reqid: 1, path: p13targetpath });
                        if (files.serverIsRecording == true) { QV('filesRecordIcon', true); }
                    }
                    break;
                default:
                    //console.log('Unknown onFilesStateChange state', state);
                    break;
            }
        }

        function CreateRemoteFiles(onFileUpdate) {
            var obj = { protocol: 5 };
            obj.onFileUpdate = onFileUpdate;
            obj.xxStateChange = function (state) { }
            obj.ProcessData = function (data) { obj.onFileUpdate(data); }
            return obj;
        }

        // Debug Only
        var autoConnectFilesTimer = null;
        function autoConnectFiles(e) { if (autoConnectFilesTimer == null) { autoConnectFilesTimer = setInterval(connectFiles, 100); } else { clearInterval(autoConnectFilesTimer); autoConnectFilesTimer = null; } }

        // 1 = Agent, 2 = SFTP
        function connectFiles(e, contype, consent) {
            p13clearConsoleMsg();
            if (!files) {
                // Setup a mesh agent files
                files = CreateAgentRedirect(meshserver, CreateRemoteFiles(p13gotFiles), serverPublicNamePort, authCookie, authRelayCookie, domainUrl);
                if (contype == 2) { files.urlname = 'sshfilesrelay.ashx'; } // If this is a SSH session, change the URL to the SSH application relay.
                files.contype = contype;
                files.options = { consent: consent }
                files.attemptWebRTC = attemptWebRTC;
                files.webrtcconfig = webrtcconfiguration;
                files.onStateChanged = onFilesStateChange;
                files.onConsoleMessageChange = function () {
                    if (files.consoleMessage) {
                        Q('p13FilesConsoleMsg').innerHTML += formatAgentConsoleMessage(files.consoleMessage, files.consoleMessageId, files.consoleMessageArgs);
                        QV('p13FilesConsoleMsg', true);
                        if (p13FilesConsoleMsgTimer != null) { clearTimeout(p13FilesConsoleMsgTimer); }
                        if (files.consoleMessageTimeout) { p13FilesConsoleMsgTimer = setTimeout(p13clearConsoleMsg, files.consoleMessageTimeout * 1000); }
                    } else {
                        p13clearConsoleMsg();
                    }
                }
                files.Start(filesNode._id);
            } else {
                //QH('Term', '');
                files.Stop();
                files = null;
            }
            p13clipboard = p13clipboardFolder = null;
            p13clipboardCut = 0;
            p13updateClipview();
        }

        var p13filetree = null;
        var p13targetpath = null;
        var p13filetreelocation = [];

        function p13fileOperationDialogEx(b) { if ((b == 0) && (files != null)) { files.sendText({ action: 'cancel' }); } }

        function p13gotFiles(data) {
            if ((data.length > 0) && (data.charCodeAt(0) != 123)) { p13gotDownloadBinaryData(data); return; } // This is ok because 4 first bytes is a control value.
            //console.log('p13gotFiles', data);
            try { data = JSON.parse(decode_utf8(data)); } catch (ex) {
                try { data = JSON.parse(data); } catch (ex) { console.log('Unable to parse: ' + data); return; }
            }
            if (data.action == 'download') { p13gotDownloadCommand(data); return; }

            // Find file result
            if (data.action == 'findfile') {
                if (xxdialogTag == data.reqid) {
                    if (data.r == null) {
                        QE('d2findFilter', true);
                        QE('filefind_dlgOkButton', true);
                        xxdialogTag = null;
                        if (Q('d2findResults').innerHTML == '') { QH('d2findResults', '<div style=text-align:center;margin:10px><i>' + "No se encontraron archivos" + '</i></div>'); }
                    } else {
                        QA('d2findResults', '<div style=white-space:nowrap>' + EscapeHtml(data.r) + '</div>');
                    }
                }
                return;
            }

            // Process file upload commands
            if ((data.action != null) && (data.action.startsWith('upload'))) { p13gotUploadData(data); return; }

            // Process any SSH actions
            switch (data.action) {
                case 'sshauth': { sshTunnelAuthDialog(data, p13sshConnectEx); return; }
                case 'autherror': { p13setConsoleMsg("Error de Autenticación", 5000); return; }
                case 'connectionerror': { p13setConsoleMsg("Error de Conexión", 5000); return; }
                case 'sessionerror': { p13setConsoleMsg("Sesión expirada", 5000); return; }
                case 'sessiontimeout': { p13setConsoleMsg("Hora de término de la sesión", 5000); return; }
            }

            // Display a dialog message
            if (data.action == 'dialogmessage') {
                if ((data.msg == null) && (xxdialogTag == 'fileMsgDialog')) {
                    setDialogMode(0); // Close the dialog box
                } else if ((data.msg == 'zipping') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Operación de Archivo", '<div style=margin:10px>' + "Comprimiendo archivos..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13fileOperationDialogEx(10));
                } else if ((data.msg == 'unzipping') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Operación de Archivo", '<div style=margin:10px>' + "Unzipping file..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else if ((data.msg == 'unziperror') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Operación de Archivo", '<div style=margin:10px>' + "Unzipping Error" + '</div><br />' + EscapeHtml(data.error));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else if ((data.msg == 'zippingFile') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Operación de Archivo", '<div style=margin:10px>' + EscapeHtml(data.file) + '</div><br /><progress value=' + EscapeHtml(data.progress) + ' style=width:100% max=100 />');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13fileOperationDialogEx(10));
                }
                return;
            }

            // Refresh file folder
            if (data.action == 'refresh') { p13folderup(9999); return; }

            if (data.path != null) {
                if (data.dir == null) {
                    // Path not found, go up directory tree until we find a valid path
                    if (p13targetpath != '') { 
                        var pathParts = p13targetpath.split('/').filter(function(p) { return p != ''; });
                        if (pathParts.length > 0) {
                            pathParts.pop(); // Remove last part
                            p13targetpath = pathParts.join('/');
                            p13requestFolderPath(p13targetpath);
                        } else {
                            // Already at root, just refresh to root
                            p13targetpath = '';
                            p13requestFolderPath(p13targetpath);
                        }
                    }
                } else {
                    data.path = data.path.replace(/\//g, '\\');
                    if ((p13filetree != null) && (data.path == p13filetree.path)) {
                        // This is an update to the same folder
                        var checkedNames = p13getCheckedNames();
                        p13filetree = data;
                        p13updateFiles(checkedNames);
                    } else {
                        // Make both paths use the same seperator not start with /
                        var x1 = data.path.replace(/\//g, '\\'), x2 = p13targetpath.replace(/\//g, '\\');
                        while ((x1.length > 0) && (x1[0] == '\\')) { x1 = x1.substring(1); }
                        while ((x2.length > 0) && (x2[0] == '\\')) { x2 = x2.substring(1); }
                        if ((x1 == x2) || ((data.path == '\\') && (p13targetpath == ''))) {
                            // This is a different folder
                            p13filetree = data;
                            p13updateFiles();
                        }
                    }
                }
            }
        }

        function p13sshConnectEx(b) {
            if (b == 0) {
                if (files != null) { connectFiles(); } // Disconnect
            } else {
                var keep = 0;
                if (Q('dp2authmethod').value == 1) {
                    if ((features2 & 0x00400000) == 0) { keep = (Q('dp2keep').checked ? 1 : 0); }
                    files.socket.send(JSON.stringify({ action: 'sshauth', username: Q('dp2user').value, password: Q('dp2pass').value, keep: keep }));
                } else if (Q('dp2authmethod').value == 3) {
                    files.socket.send(JSON.stringify({ action: 'sshkeyauth', keypass: Q('dp2keypass2').value }));
                } else {
                    if ((features2 & 0x00400000) == 0) { keep = (Q('dp2keep1').checked ? 1 : 0); if (keep == 1) { keep += (Q('dp2keep2').checked ? 1 : 0); } } // Keep: 1 = user & key, 2 = User, key and password
                    var reader = new FileReader(), username = Q('dp2user').value, keypass = Q('dp2keypass').value;
                    reader.onload = function (e) { files.socket.send(JSON.stringify({ action: 'sshauth', username: username, keypass: keypass, key: e.target.result, keep: keep })); }
                    reader.readAsText(Q('dp2key').files[0]);
                }
            }
        }

        function p13getCheckedNames() {
            // Save all existing checked boxes
            var checkedNames = [], checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { checkedNames.push(p13filetree.dir[checkboxes[i].value].n) }; }
            return checkedNames;
        }

        function p13updateFiles(checkedNames) {
            var html1 = '', html2 = '', displayPath = '', displayPathInput = '';
            if (p13filetree == null) return;
            // Work on parsing the file path
            var x = p13filetree.path.split('\\');
            p13filetreelocation = [];
            for (var i in x) { if (x[i] != '') { p13filetreelocation.push(x[i]); } } // Remove empty spaces
            // Set the breadcrumb display value
            displayPath = '<a href=# style=cursor:pointer onclick="return p13folderup(0)">' + "Raíz" + '</a>';
            for (var i in p13filetreelocation) { displayPath += ' / <a href=# style=cursor:pointer onclick="return p13folderup(' + (parseInt(i) + 1) + ')">' + EscapeHtml(p13filetreelocation[i]) + '</a>'; }
            // Set the input display value
            if (isWindowsNode(currentNode)) {
                displayPathInput = p13filetreelocation.join('\\');
                if (displayPathInput && !displayPathInput.endsWith('\\')) { displayPathInput += '\\'; }
            } else {
                displayPathInput = '/' + p13filetreelocation.join('/');
                if (displayPathInput != '/' && !displayPathInput.endsWith('/')) { displayPathInput += '/'; }
            }
            var newlinkpath = p13filetreelocation.join('/');

            // Sort the files
            var filetreexx = p13sort_files(p13filetree.dir);

            // Display all files and folders at this location
            for (var i in filetreexx) {
                // Figure out the name and shortname
                var f = filetreexx[i], name = f.n, shortname;
                // if (name.length > 70) { shortname = '<span title="' + EscapeHtml(name) + '">' + EscapeHtml(name.substring(0, 70)) + ("..." + '</span>'); } else { shortname = EscapeHtml(name); }
                // Removed redundant filename length check because we handle it in the CSS
                shortname = EscapeHtml(name);

                // Figure out the date
                var fdatestr = '&nbsp;';
                if (f.d != null) {
                    var fdate = new Date(f.d);
                    if (typeof f.d == 'number') { fdate = new Date(f.d * 1000); }
                    fdatestr = printDateTime(fdate) + '&nbsp;';
                }

                // Figure out the size
                var fsize = '';
                if (f.s != null) {
                    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                    const i = parseInt(Math.floor(Math.log(Math.abs(f.s)) / Math.log(1024)), 10);
                    const option = Q('p13sizedropdown').options[Q('p13sizedropdown').selectedIndex];
                    if (Q('p13sizedropdown').value == 0) {
                        if (f.s === 0) {
                            fsize = 'n/a';
                        } else {
                            fsize = (i === 0 ? `${f.s} ${sizes[i]}` : `${(f.s / (1024 ** i)).toFixed(2)} ${sizes[i]}`);
                        }
                    } else if (Q('p13sizedropdown').value == 1) {
                        fsize = getFileSizeStr(f.s);
                    } else {
                        fsize = `${(f.s / (2 ** option.value)).toFixed(2)} ${option.title}`;
                    }
                }

                var h = '';
                if (f.t < 3) {
                    var right = '', title = '';
                    h = '<div class=filelist file=999>';
                    h += '<input file=999 style=float:left name=fd class="fcb form-check-input m-1" type=checkbox onchange=p13setActions() value=\'' + f.nx + '\'>&nbsp;<span class=fsize>' + fdatestr + '</span><span style=float:right>' + EscapeHtml(fsize) + right + '</span>';
                    h += '<span title="' + shortname + '"><div class="fileIcon' + (f.dt == 'REMOVABLE' ? 5 : (f.dt == 'CDROM' ? 6 : f.t)) + ' mt-1" onclick=p13folderset("' + encodeURIComponentEx(f.nx) + '")></div><a href=# style=cursor:pointer onclick=\'return p13folderset("' + encodeURIComponentEx(f.nx) + '")\'>';
                    var sysvol = (isWindowsNode(currentNode) && f.dt && (DeviceDetailsNodeId == currentNode._id) && DeviceDetailsHardware && DeviceDetailsHardware.windows && DeviceDetailsHardware.windows.volumes) ? DeviceDetailsHardware.windows.volumes[shortname.charAt(0).toUpperCase()] : null;
                    if (sysvol && sysvol.name) {
                        h += EscapeHtml(sysvol.name) + ' (' + shortname + ')';
                    } else {
                        h += shortname;
                    }
                    h += '</a></span></div>';
                } else {
                    var link = shortname;
                    if (f.s > 0) {
                        if (filesNode.mtype == 3) {
                            // Local link
                            link = '<a href=# style=cursor:pointer onclick="return p13downloadfile(\'' + encodeURIComponentEx(newlinkpath + '/' + name) + '\',\'' + encodeURIComponentEx(name) + '\',' + f.s + ')">' + shortname + '</a>';
                        } else {
                            // Server link
                            //link = '<a href="devicefile.ashx?c=' + authCookie + '&m=' + currentNode.meshid.split('/')[2] + '&n=' + currentNode._id.split('/')[2] + '&f=' + encodeURIComponentEx(newlinkpath + '/' + name) + '" download="' + name + '" style=cursor:pointer>' + shortname + '</a>';
                            // Server link
                            link = '<a onclick=downloadFile("devicefile.ashx?c=' + authCookie + '&m=' + currentNode.meshid.split('/')[2] + '&n=' + currentNode._id.split('/')[2] + '&f=' + encodeURIComponentEx(newlinkpath + '/' + name) + '","' + encodeURIComponentEx(name) + '") style=cursor:pointer>' + shortname + '</a>';
                        }
                    }
                    h = '<div id=fileEntry cmenu=filesContextMenu fileIndex=' + i + ' class=filelist file=3>';
                    h += '<input file=3 style=float:left name=fd class="fcb form-check-input m-1" type=checkbox onchange=p13setActions() value=\'' + f.nx + '\'>&nbsp;';
                    h += '<span class=fsize>' + fdatestr + '</span>';
                    h += '<span style=float:right>' + EscapeHtml(fsize) + '</span>';
                    h += '<span title="' + shortname + '"><div class="fileIcon' + f.t + ' mt-1"></div>' + link + '</span>';
                    h += '</div>';
                }

                if (f.t < 3) { html1 += h; } else { html2 += h; }
            }

            // Display the files and path
            QH('p13files', html1 + html2);
            QH('p13currentpath', displayPath);
            Q('p13currentpathinput').value = displayPathInput;
            QE('p13FolderUp', p13filetreelocation.length != 0);

            // Re-check all boxes if needed using names
            if (checkedNames != null) { var checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { if (checkedNames.indexOf(p13filetree.dir[checkboxes[i].value].n) >= 0) { checkboxes[i].checked = true; } } }

            // Update the actions buttons
            p13setActions();
        }

        function p13openfilefolder() {
            setModalContent('xxAddAgent', "Open File/Folder", "Are you sure you want to open this file/folder on the remote devices desktop?");
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13openfilefolderEx());
        }
        function p13openfilefolderEx() {
            var openfilefolder = "", checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    openfilefolder = (isWindowsNode(currentNode) ? '' : '/') + p13filetreelocation.join(isWindowsNode(currentNode) ? '\\' : '/') + (isWindowsNode(currentNode) ? '\\' : '/') + p13filetree.dir[checkboxes[i].value].n;
                }
            }
            if (openfilefolder == "") return;
            files.sendText({ action: 'open', reqid: 1, path: openfilefolder, dir: (p13getFileSelDirCount() == 1) ? true : false });
        }

        // Path input field handlers
        function p13pathRowClick(event) {
            if (event && event.target && event.target.closest && (event.target.closest('.toright2') || event.target.closest('select') || event.target.closest('input') || event.target.closest('button'))) { return; }
            if ((!event || event.type !== 'dblclick') && event && event.target && ((event.target.tagName == 'A') || (event.target.closest && event.target.closest('a')))) { return; }
            p13showPathInput();
        }

        function p13showPathInput() {
            var pathSpan = Q('p13currentpath');
            var pathInput = Q('p13currentpathinput');
            pathSpan.style.display = 'none';
            pathInput.style.display = 'inline-block';
            pathInput.focus();
            pathInput.select();
        }

        function p13hidePathInput() {
            var pathSpan = Q('p13currentpath');
            var pathInput = Q('p13currentpathinput');
            pathInput.style.display = 'none';
            pathSpan.style.display = '';
        }

        function p13onPathKeyDown(event) {
            if (event.key === 'Enter') {
                event.preventDefault();
                var pathInput = Q('p13currentpathinput');
                var newPath = pathInput.value.trim();
                if (newPath) {
                    p13targetpath = newPath.split('\\').join('/');
                    // Remove trailing slash for consistency
                    if (p13targetpath.length > 1 && p13targetpath.endsWith('/')) {
                        p13targetpath = p13targetpath.substring(0, p13targetpath.length - 1);
                    }
                    p13requestFolderPath(p13targetpath);
                }
                p13hidePathInput();
                return false;
            } else if (event.key === 'Escape') {
                event.preventDefault();
                p13hidePathInput();
                p13updateFiles();
                return false;
            }
            return true;
        }

        function p13requestFolderPath(path) {
            if (files) { p13storeCurrentPath(path); files.sendText({ action: 'ls', reqid: 1, path: path }); }
        }

        function p13gotofolder() {
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Go To Folder", '<input type=text id=p13folderinput style=width:100% value="' + (isWindowsNode(currentNode) ? p13targetpath.replaceAll('/','\\') + '\\' : p13targetpath + '/') +'"placeholder="' + (isWindowsNode(currentNode) ? 'C:\\' : '/var/www') + '" />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p13gotofolderEx);
            focusTextBox('p13folderinput');
        }
        function p13gotofolderEx() {
            p13targetpath = Q('p13folderinput').value.split('\\').join('/');
            p13requestFolderPath(p13targetpath);
        }

        function p13folderset(x) {
            p13targetpath = joinPaths(p13filetree.path, p13filetree.dir[x].n).split('\\').join('/');
            p13requestFolderPath(p13targetpath);
        }

        function p13folderup(x) {
            if (x == null) { p13filetreelocation.pop(); } else { while (p13filetreelocation.length > x) { p13filetreelocation.pop(); } }
            p13targetpath = p13filetreelocation.join('/');
            p13requestFolderPath(p13targetpath);
            return false;
        }

        // Store the current path for a given node as browser state.
        // This is so, when reconnecting to a device, you go back to the same path.
        function p13storeCurrentPath(path) {
            var filepaths = [], j = -1;
            try { filepaths = JSON.parse(getstore('_devFilePaths', '[]')); } catch (ex) { }
            for (var i = 0; i < filepaths.length; i++) { if (filepaths[i].n == currentNode._id) { j = i; } }
            if (j >= 0) { filepaths.splice(j, 1); }
            filepaths.push({ n: currentNode._id, p: path });
            while (filepaths.length > 40) { filepaths.shift(); } // Keep only 40 devices worth of paths.
            putstore('_devFilePaths', JSON.stringify(filepaths));
        }

        function p13findfile() {
            if (xxdialogMode) return;
            var x = addHtmlFormFloating("Filtrar", '<input id=d2findFilter class="form-control" onkeyup=p13findfileValidate() onkeydown=p13findfileDown(event) placeholder="*.txt"></input>');
            x += '<div id=d2findResults style="width:100%;height:184px;background-color:white;border:1px solid black;padding:3px;overflow:scroll;margin-top:8px"></div>';
            x += '<div style=margin-top:8px><input id=filefind_dlgCancelButton type=button value=' + "Cerrar" + ' style=float:right;width:80px;margin-left:5px onclick=p13findfileCancel()>';
            x += '<input id=filefind_dlgOkButton type=submit value=' + "Buscar" + ' style=float:right;width:80px onclick=p13findfileEx()><br /><br /></div>';
            setModalContent('xxAddAgent', "Buscar Archivos", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            p13findfileValidate();
            Q('d2findFilter').focus();
        }

        function p13findfileDown(e) { if (e.code == "Enter") { p13findfileEx(); } }
        function p13findfileValidate() { QE('filefind_dlgOkButton', Q('d2findFilter').value.length > 0); }
        function p13findfileCancel() { if (xxdialogTag != null) { files.sendText({ action: 'cancelfindfile', reqid: xxdialogTag }); } dialogclose(0) }

        function p13findfileEx(b, t) {
            if (Q('d2findFilter').value.length == 0) return;
            var winAgent = isWindowsNode(currentNode);
            var slash = winAgent ? '\\' : '/';
            var path = p13filetreelocation.join(slash) + slash;
            if (!winAgent) { path = slash + path; }
            xxdialogTag = 'find:' + Math.random();
            files.sendText({ action: 'findfile', reqid: xxdialogTag, path: path, filter: Q('d2findFilter').value });
            QH('d2findResults', '');
            QE('d2findFilter', false);
            QE('filefind_dlgOkButton', false);
        }

        var p13sortorder;
        function p13sort_filename(a, b) { if (a.ln > b.ln) return (1 * p13sortorder); if (a.ln < b.ln) return (-1 * p13sortorder); return 0; }
        function p13sort_timestamp(a, b) { if (a.d > b.d) return (1 * p13sortorder); if (a.d < b.d) return (-1 * p13sortorder); return 0; }
        function p13sort_bysize(a, b) { if (a.s == b.s) return p13sort_filename(a, b); return (((a.s - b.s)) * p13sortorder); }

        function p13sort_files(files) {
            var r = [], sortselection = Q('p13sortdropdown').value;
            for (var i in files) { files[i].nx = i; if (files[i].s == null) { files[i].s = 0; } if (files[i].n == null) { files[i].n = i; } files[i].ln = files[i].n.toLowerCase(); r.push(files[i]); }
            p13sortorder = 1;
            if (sortselection > 3) { p13sortorder = -1; sortselection -= 3; }
            if (sortselection == 1) { r.sort(p13sort_filename); }
            else if (sortselection == 2) { r.sort(p13sort_bysize); }
            else if (sortselection == 3) { r.sort(p13sort_timestamp); }
            return r;
        }

        function p13setActions() {
            var advancedFeatures = ((currentNode.agent != null) && (currentNode.agent.id != 14)); // Reduce file feature on some devices.
            if (p13filetree == null) {
                QE('p13DeleteFileButton', false);
                QE('p13NewFolderButton', false);
                QE('p13UploadButton', false);
                QE('p13NewFileButton', false);
                QE('p13RenameFileButton', false);
                QE('p13ViewFileButton', false);
                QE('p13SelectAllButton', false);
                Q('p13SelectAllButton').value = "Seleccionar Todo";
                QE('p13RefreshButton', false);
                QE('p13FindButton', false);
                QE('p13CutButton', false);
                QE('p13CopyButton', false);
                QE('p13ZipButton', false);
                QE('p13UnZipButton', false);
                QE('p13PasteButton', false);
                QE('p13GoToFolderButton', false);
                QE('p13DownloadButton', false);
                QE('p13OpenButton', false);
            } else {
                var cc = p13getFileSelCount(), tc = p13getFileCount(), sfc = p13getFileSelCount(false); // In order: number of entires selected, number of total entries, number of selected entires that are files (not folders)
                var winAgent = isWindowsNode(currentNode);
                QE('p13DeleteFileButton', (cc > 0) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13NewFolderButton', advancedFeatures && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13UploadButton', ((p13filetreelocation.length > 0) || (winAgent == false) || (currentNode.agent.id == 14)));
                QE('p13NewFileButton', advancedFeatures && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13RenameFileButton', advancedFeatures && (cc == 1) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13ViewFileButton', advancedFeatures && (cc == 1) && (sfc == 1) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13SelectAllButton', tc > 0);
                Q('p13SelectAllButton').value = (cc > 0 ? "No seleccionar nada" : "Seleccionar Todo");
                QE('p13RefreshButton', true);
                QE('p13FindButton', advancedFeatures && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13CutButton', advancedFeatures && (cc > 0) && (cc == sfc) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13CopyButton', advancedFeatures && (cc > 0) && (cc == sfc) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13ZipButton', advancedFeatures && (cc > 0) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13UnzipButton', advancedFeatures && (cc == 1) && (sfc == 1) && ((p13filetreelocation.length > 0) || (winAgent == false)) && p13getFileSelAllowedExt('.zip'));
                QE('p13PasteButton', advancedFeatures && ((p13filetreelocation.length > 0) || (winAgent == false)) && ((p13clipboard != null) && (p13clipboard.length > 0)));
                QE('p13GoToFolderButton', advancedFeatures);
                QE('p13OpenButton', advancedFeatures && (cc == 1));
                QE('p13DownloadButton', (cc > 0) && (cc == sfc) && ((p13filetreelocation.length > 0) || (winAgent == false)));
            }
            var filesState = ((files != null) && (files.state != 0));
            if (((filesState == true) && (files.contype != 2)) || (filesNode.agent == null) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
                QH('filesCustomUpperRight', '');
            } else {
                QH('filesCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("Puerto SSH {0}", (filesNode.sshport ? filesNode.sshport : 22)) + '</a>');
            }
            QV('filesActionsBtn', filesNode.mtype != 3);
            QV('p13FindButton', filesNode.mtype != 3);
            QV('p13CutButton', filesNode.mtype != 3);
            QV('p13CopyButton', filesNode.mtype != 3);
            QV('p13ZipButton', filesNode.mtype != 3);
            QV('p13UnzipButton', filesNode.mtype != 3);
            QV('p13PasteButton', filesNode.mtype != 3);
            [
                ['p13NewFolderButton', 'p13MobileNewFolderButton'],
                ['p13NewFileButton', 'p13MobileNewFileButton'],
                ['p13UploadButton', 'p13MobileUploadButton'],
                ['p13DownloadButton', 'p13MobileDownloadButton'],
                ['p13CutButton', 'p13MobileCutButton'],
                ['p13CopyButton', 'p13MobileCopyButton'],
                ['p13PasteButton', 'p13MobilePasteButton'],
                ['p13ZipButton', 'p13MobileZipButton'],
                ['p13UnzipButton', 'p13MobileUnzipButton'],
                ['p13RefreshButton', 'p13MobileRefreshButton'],
                ['p13FindButton', 'p13MobileFindButton'],
                ['p13GoToFolderButton', 'p13MobileGoToFolderButton'],
                ['p13OpenButton', 'p13MobileOpenButton']
            ].forEach(function (x) { QE(x[1], !Q(x[0]).disabled); });
        }

        function p13getFileSelCount(includeDirs) { var cc = 0; var checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && ((includeDirs != false) || (checkboxes[i].attributes.file.value == '3'))) cc++; } return cc; }
        function p13getFileSelDirCount() { var cc = 0, checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && (checkboxes[i].attributes.file.value == '999')) cc++; } return cc; }
        function p13getFileCount() { var cc = 0; var checkboxes = document.getElementsByName('fd'); return checkboxes.length; }
        function p13getFileSelAllowedExt(ext) { var checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && (!p13filetree.dir[checkboxes[i].value].n.endsWith(ext))) return false; } return true; }
        function p13selectallfile() { var nv = (p13getFileSelCount() == 0), checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = nv; } p13setActions(); }
        function p13createfolder() {
            setModalContent('xxAddAgent', "Nueva Carpeta", '<input type=text id=p13renameinput maxlength=64 onkeyup=p13fileNameCheck(event) style=width:100% />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13createfolderEx());
            focusTextBox('p13renameinput'); p13fileNameCheck();
        }
        function p13createfolderEx() { files.sendText({ action: 'mkdir', reqid: 1, path: p13filetreelocation.join('/') + '/' + Q('p13renameinput').value }); p13folderup(999); }
        function p13createfile() {
            setModalContent('xxAddAgent', "Nuevo Archivo", '<input type=text id=p13renameinput maxlength=64 onkeyup=p13fileNameCheck(event) style=width:100% />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13createfileEx());
            focusTextBox('p13renameinput'); p13fileNameCheck();
        }
        function p13createfileEx() { files.sendText({ action: 'mkfile', reqid: 1, path: p13filetreelocation.join('/') + '/' + Q('p13renameinput').value }); p13folderup(999); }
        function p13deletefile() {
            var cc = p13getFileSelCount(), rec = (p13getFileSelDirCount() > 0) ? '<br /><br /><label><input type=checkbox class="form-check-input me-2" id=p13recdeleteinput>' + "Eliminado recursivo" + '</label><br>' : '<input type=checkbox class="form-check-input me-2" id=p13recdeleteinput style=\'display:none\'>';
            setModalContent('xxAddAgent', "Eliminar", (cc > 1) ? (format("¿Eliminar {0} elementos seleccionados?", cc) + rec) : ("¿Eliminar elemento seleccionado?" + rec));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13deletefileEx());
        }
        function p13deletefileEx() { var delfiles = [], checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { delfiles.push(p13filetree.dir[checkboxes[i].value].n); } } files.sendText({ action: 'rm', reqid: 1, path: p13filetreelocation.join('/'), delfiles: delfiles, rec: Q('p13recdeleteinput').checked }); p13folderup(999); }
        function p13renamefile() {
            var renamefile, checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    renamefile = p13filetree.dir[checkboxes[i].value].n;
                }
            }
            setModalContent('xxAddAgent', "Renombrar", '<input type=text id=p13renameinput maxlength=64 onkeyup=p13fileNameCheck(event) style=width:100% value="' + renamefile + '" />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13renamefileEx(3, { action: 'rename', path: p13filetreelocation.join('/'), oldname: renamefile }));
            focusTextBox('p13renameinput');
            p13fileNameCheck();
        }
        function p13renamefileEx(b, t) { t.newname = Q('p13renameinput').value; files.sendText(t); p13folderup(999); }
        function p13fileNameCheck(e) { var x = isFilenameValid(Q('p13renameinput').value); QE('idx_dlgOkButton', x); if ((x == true) && (e != null) && (e.keyCode == 13)) { dialogclose(1); } }
        function p13uploadFile() {
            setModalContent('xxAddAgent', "Cargar Archivo", '<input type=file name=files id=p13uploadinput class="form-control" multiple=multiple onchange="updateUploadDialogOk(\'p13uploadinput\')" />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13uploadFileEx());
            updateUploadDialogOk('p13uploadinput');
        }
        function updateUploadDialogOk(x) { QE('idx_dlgOkButton', Q('p13uploadinput').files.length > 0); }
        function p13uploadFileEx() { p13doUploadFiles(Q('p13uploadinput').files); return false; }
        function p13viewfile() {
            var checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    if (p13filetree.dir[checkboxes[i].value].s <= 204800) {
                        p13downloadfile(encodeURIComponentEx(p13filetreelocation.join('/') + '/' + p13filetree.dir[checkboxes[i].value].n), encodeURIComponentEx(p13filetree.dir[checkboxes[i].value].n), p13filetree.dir[checkboxes[i].value].s, 'viewer');
                    } else { messagebox("Editor de Archivos", "Solo se pueden editar archivos de menos de 200k."); }
                    break;
                }
            }
        }

        function p13unzipFile() {
            var dest, input, checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    var slash = isWindowsNode(currentNode) ? '\\' : '/';
                    dest = (isWindowsNode(currentNode) ? '' : '/') + p13filetreelocation.join(slash) + slash + p13filetree.dir[checkboxes[i].value].n.split('.zip')[0] + slash;
                    input = (isWindowsNode(currentNode) ? '' : '/') + p13filetreelocation.join(slash) + slash + p13filetree.dir[checkboxes[i].value].n;
                }
            }
            setModalContent('xxAddAgent', "Unzip To Folder", '<input type=text id=p13unzipfolderinput maxlength=64 style=width:100% value="' + dest + '" />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p13unzipFileEx(3, { action: 'unzip', input: input }));
            focusTextBox('p13unzipfolderinput');
        }

        function p13unzipFileEx(a, tag) {
            tag.dest = Q('p13unzipfolderinput').value;
            files.sendText(tag);
        }

        function p13zipFiles() {
            var inputFiles = [], checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { inputFiles.push(p13filetree.dir[checkboxes[i].value].n); } }
            setModalContent('xxAddAgent', "Nombre de Archivo ZIP", '<input type=text id=p13renameinput maxlength=64 onkeyup=p13fileNameCheck(event) style=width:100% value="" />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13zipFilesEx(3, { action: 'zip', path: p13filetreelocation.join('/'), files: inputFiles }));
            focusTextBox('p13renameinput');
            p13fileNameCheck();
        }

        function p13zipFilesEx(b, tag) {
            tag.output = Q('p13renameinput').value;
            if (!tag.output.toLowerCase().endsWith('.zip')) { tag.output += '.zip'; }
            files.sendText(tag);
        }

        var p13clipboard = null, p13clipboardFolder = null, p13clipboardCut = 0;
        function p13copyFile(cut) { var checkboxes = document.getElementsByName('fd'); p13clipboard = []; p13clipboardCut = cut, p13clipboardFolder = p13targetpath; for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && (checkboxes[i].attributes.file.value == '3')) { p13clipboard.push(p13filetree.dir[checkboxes[i].value].n); } } p13updateClipview(); }
        function p13pasteFile() {
            var x = '';
            if ((p13clipboard != null) && (p13clipboard.length > 0)) {
                if (p13clipboardCut == 0) {
                    if (p13clipboard.length > 1) { x = format("¿Confirmar la copia de {1} entradas a esta ubicación?", p13clipboard.length); } else { x = format("¿Confirmar la copia de 1 entrada a esta ubicación?"); }
                } else {
                    if (p13clipboard.length > 1) { x = format("¿Confirmar el movimiento de {1} entradas a esta ubicación?", p13clipboard.length); } else { x = format("¿Confirmar el movimiento de 1 entrada a esta ubicación?"); }
                }
            }
            setModalContent('xxAddAgent', "Pegar", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13pasteFileEx());
        }
        function p13pasteFileEx() { files.sendText({ action: (p13clipboardCut == 0 ? 'copy' : 'move'), reqid: 1, scpath: p13clipboardFolder, dspath: p13targetpath, names: p13clipboard }); p13folderup(999); if (p13clipboardCut == 1) { p13clipboard = null, p13clipboardFolder = null, p13clipboardCut = 0; p13updateClipview(); } }
        function p13updateClipview() {
            var x = '';
            if ((p13clipboard != null) && (p13clipboard.length > 0)) {
                if (p13clipboardCut == 0) {
                    if (p13clipboard.length > 1) {
                        x = format("Manteniendo {0} entradas para copiar" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Eliminar" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Manteniendo 1 entrada para copiar" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Eliminar" + '</a>.');
                    }
                } else {
                    if (p13clipboard.length > 1) {
                        x = format("Manteniendo {0} entradas para mover" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Eliminar" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Manteniendo 1 entrada para mover" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Eliminar" + '</a>.');
                    }
                }
            }
            QH('p13bottomstatus', x);
            p13setActions();
        }
        function p13clearClip() { p13clipboard = null; p13clipboardFolder = null; p13clipboardCut = 0; p13updateClipview(); return false; }

        function p13fileDragDrop(e) {
            haltEvent(e);
            QV('p13bigfail', false);
            QV('p13bigok', false);
            if ((e.dataTransfer == null) || (e.dataTransfer.files.length == 0) || (p13filetree == null)) return;

            // Check if these are files we can upload, remove all folders.
            var files = [];
            for (var i in e.dataTransfer.files) { if ((e.dataTransfer.files[i].size != null) && (e.dataTransfer.files[i].size != 0)) { files.push(e.dataTransfer.files[i]); } }
            if (files.length == 0) return;

            p13doUploadFiles(files);
        }

        var p13dragtimer = null;
        function p13fileDragOver(e) {
            haltEvent(e);
            if (p13dragtimer != null) { clearTimeout(p13dragtimer); p13dragtimer = null; }
            var ac = (p13filetree != null); // Set to true if we can accept the file
            QV('p13bigok', ac);
            QV('p13bigfail', !ac);
        }

        function p13fileDragLeave(e) {
            haltEvent(e);
            if (e.target.id != 'p13filetable') {
                QV('p13bigfail', false);
                QV('p13bigok', false);
            } else {
                p13dragtimer = setTimeout(function () { QV('p13bigfail', false); QV('p13bigok', false); p13dragtimer = null; }, 10);
            }
        }

        //
        // FILES DOWNLOAD
        //

        var gdownloadFile; // Global state for file download

        // Called by the html page to start a download, arguments are: path, file name and file size.
        function p13downloadfile(x, y, z, tag) {
            if (xxdialogMode || !files) return;
            gdownloadFile = { path: decodeURIComponent(x), file: decodeURIComponent(y), size: z, tsize: 0, data: '', state: 0, id: Math.random(), tag: tag }
            //console.log('p13downloadFileCancel', gdownloadFile);
            files.sendText({ action: 'download', sub: 'start', id: gdownloadFile.id, path: gdownloadFile.path });
            setModalContent('xxAddAgent', "Descargar Archivo", '<div>' + gdownloadFile.file + '</div><br /><progress id=d2progressBar style=width:100% value=0 max=' + z + ' />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13downloadFileCancel());
        }

        // Called by the html page to cancel the download
        function p13downloadFileCancel() {
            setDialogMode(0);
            files.sendText({ action: 'download', sub: 'cancel', id: gdownloadFile.id }); gdownloadFile = null;
        }

        // Called by the transport when download control command is received
        function p13gotDownloadCommand(cmd) {
            //console.log('p13gotDownloadCommand', cmd);
            if ((gdownloadFile == null) || (cmd.id != gdownloadFile.id)) return;
            if (cmd.sub == 'start') { gdownloadFile.state = 1; files.sendText({ action: 'download', sub: 'startack', id: gdownloadFile.id }); }
            else if (cmd.sub == 'cancel') { gdownloadFile = null; setDialogMode(0); }
        }

        // Called by the transport when binary data is received
        function p13gotDownloadBinaryData(data) {
            if (!gdownloadFile || gdownloadFile.state == 0) return;
            if (data.length > 4) {
                gdownloadFile.tsize += (data.length - 4); // Add to the total bytes received
                gdownloadFile.data += data.substring(4); // Append the data
                Q('d2progressBar').value = gdownloadFile.tsize; // Change the progress bar
            }
            if ((ReadInt(data, 0) & 1) != 0) { // Check end flag
                if (gdownloadFile.tag == 'viewer') {
                    // View the file in the dialog box
                    setModalContent('xxAddAgent', EscapeHtml(gdownloadFile.file), 4, 'extra-large');
                    var file = gdownloadFile.file;
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13editSaveBack(3, file));
                    const crlf = (gdownloadFile.data.match(/\r\n/g) || []).length;
                    const cr = (gdownloadFile.data.match(/\r(?!\n)/g) || []).length;
                    const lf = (gdownloadFile.data.match(/(?<!\r)\n/g) || []).length;
                    let type;
                    if (crlf > 0 && cr === 0 && lf === 0) {
                        type = 'crlf';
                    } else if (lf > 0 && crlf === 0 && cr === 0) {
                        type = 'lf';
                    } else if (cr > 0 && crlf === 0 && lf === 0) {
                        type = 'cr';
                    } else {
                        // Mixed - pick dominant
                        const dominant = Math.max(crlf, cr, lf);
                        if (dominant === crlf) type = 'crlf';
                        else if (dominant === lf) type = 'lf';
                        else type = 'cr';
                    }
                    if (type === 'crlf') {
                        d4EditLineBreakVal = 0; // Windows (CR LF)
                    } else if (type === 'lf') {
                        d4EditLineBreakVal = 1; // Linux (LF)
                    } else {
                        d4EditLineBreakVal = 2; // Mac (CR)
                    }
                    d4ToggleLineBreak(true);
                    QV('d4EncodingButton', true);
                    QV('d4LineBreakButton', true);
                    if (d4EditEncodingVal == 1) {
                        Q('d4editorarea').value = decode_utf8(gdownloadFile.data); // UTF8 Encoding
                    } else {
                        Q('d4editorarea').value = gdownloadFile.data; // RAW Encoding
                    }
                    gdownloadFile = null;
                } else {
                    // Save the file to disk
                    saveAs(data2blob(gdownloadFile.data), gdownloadFile.file); gdownloadFile = null; setDialogMode(0); // Save the file
                }
            } else {
                files.sendText({ action: 'download', sub: 'ack', id: gdownloadFile.id }); // Send the ACK
            }
        }

        function p13downloadButton() {
            var checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    var thefile = p13filetree.dir[checkboxes[i].value];
                    var link = 'devicefile.ashx?c=' + authCookie;
                    link += '&m=' + currentNode.meshid.split('/')[2];
                    link += '&n=' + currentNode._id.split('/')[2];
                    link += '&f=' + encodeURIComponentEx(p13filetreelocation.join('/') + '/' + thefile.n);
                    downloadFile(link, encodeURIComponentEx(thefile.n));
                }
            }
        }

        var d4EditWrapVal = 0;
        var d4EditSizeVal = 0;
        var d4EditEncodingVal = 0;
        var d4EditLineBreakVal = 0;
        function d4ToggleWrap(update) {
            if (!update) { d4EditWrapVal = ++d4EditWrapVal % 2; }
            Q('d4WrapButton').value = ["Envoltura: ON", "Envoltura: OFF"][d4EditWrapVal];
            QS('d4editorarea').overflow = (d4EditWrapVal == 0) ? 'auto' : 'scroll';
            QS('d4editorarea')['white-space'] = (d4EditWrapVal == 0) ? null : 'pre';
            putstore('editorWrap', d4EditWrapVal);
        }

        function d4ToggleSize(update) {
            if (!update) { d4EditSizeVal = ++d4EditSizeVal % 4; }
            QS('d4editorarea')['font-size'] = ['100%', '125%', '150%', '200%'][d4EditSizeVal];
            Q('d4SizeButton').value = ["Tamaño: 100%", "Tamaño: 125%", "Tamaño: 150%", "Tamaño: 200%"][d4EditSizeVal];
            putstore('editorSize', d4EditSizeVal);
        }

        function d4ToggleEncoding(update) {
            if (!update) {
                d4EditEncodingVal = ++d4EditEncodingVal % 2;
                if (d4EditEncodingVal == 1) {
                    Q('d4editorarea').value = decode_utf8(Q('d4editorarea').value);
                } else {
                    Q('d4editorarea').value = encode_utf8(Q('d4editorarea').value);
                }
            }
            Q('d4EncodingButton').value = ["Codificación: RAW", "Codificación: UTF8"][d4EditEncodingVal];
            putstore('editorEncoding', d4EditEncodingVal);
        }

        function d4ToggleLineBreak(update) {
            if (!update) {
                d4EditLineBreakVal = ++d4EditLineBreakVal % 3;
            }
            Q('d4LineBreakButton').value = ["Salto de línea: Windows (CR LF)", "Salto de línea: Linux (LF)", "Salto de línea: Mac (CR)"][d4EditLineBreakVal];
            putstore('editorLineBreak', d4EditLineBreakVal);
        }

        function p13editSaveBack(b, tag) {
            var data;
            var value = Q('d4editorarea').value;
            value = d4EditLineBreakVal === 0 ? value.replace(/\r?\n|\r/g, '\r\n') : // Windows
                d4EditLineBreakVal === 2 ? value.replace(/\r\n|\n/g, '\r') : // Mac
                    value.replace(/\r\n|\r/g, '\n'); // Linux
            if (d4EditEncodingVal == 1) {
                data = new TextEncoder().encode(value); // UTF8 encoding
            } else {
                data = new TextEncoder().encode(decode_utf8(value)); // RAW encoding
            }
            p13uploadFileContinue(1, [{ name: tag, size: data.byteLength, type: 'text/plain', xdata: data }]);
        }

        //
        // FILES UPLOAD
        //

        var uploadFile;
        function p13doUploadFiles(files) {
            if (xxdialogMode) return;

            // Check if we are going to overwrite any files
            var winAgent = isWindowsNode(currentNode);
            var targetFiles = [], overWriteCount = 0;
            for (var i in p13filetree.dir) { if (winAgent) { targetFiles.push(p13filetree.dir[i].n.toLowerCase()); } else { targetFiles.push(p13filetree.dir[i].n); } }
            for (var i = 0; i < files.length; i++) {
                if (winAgent) {
                    if (targetFiles.indexOf(files[i].name.toLowerCase()) >= 0) { overWriteCount++; }
                } else {
                    if (targetFiles.indexOf(files[i].name) >= 0) { overWriteCount++; }
                }
            }

            if (overWriteCount == 0) {
                // If no overwrite, go ahead with upload
                p13uploadFileContinue(1, files);
            } else {
                // Otherwise, prompt for confirmation
                setModalContent('xxAddAgent', "Cargar Archivo", format((overWriteCount == 1) ? "La carga sobrescribirá 1 archivo. ¿Continuar?" : "La carga sobrescribirá {0} archivos. ¿Continuar?", overWriteCount));
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13uploadFileContinue(3, files));
            }
        }

        function p13uploadFileContinue(b, files) {
            uploadFile = {};
            uploadFile.xpath = p13filetreelocation.join('/');
            uploadFile.xfiles = files;
            uploadFile.xfilePtr = -1;
            setModalContent('xxAddAgent', "Cargar Archivo", '<div id=p13dfileName>' + "Conectando...." + '</div><br /><progress id=d2progressBar style=width:100% value=0 max=0 />');
            showModal('xxAddAgentModal', 'idx_dlgCancelButton', () => p13uploadFileCancel(10));
            document.getElementById('idx_dlgOkButton').style.display = 'none'; // hide OK button
            p13uploadNextFile();
            if (b == 3) return false; // if overwritten, dont close the dialog
        }

        // Perform SHA-384 hashing
        const byteToHex = [];
        for (var n = 0; n <= 0xff; ++n) { var hexOctet = n.toString(16).padStart(2, '0'); byteToHex.push(hexOctet); }
        function arrayBufferToHex(arrayBuffer) { return Array.prototype.map.call(new Uint8Array(arrayBuffer), n => byteToHex[n]).join(''); }
        function performHash(data, f) { window.crypto.subtle.digest('SHA-384', data).then(function (v) { f(arrayBufferToHex(v)); }, function () { f(null); }); }
        function performHashOnFile(file, f) {
            // TODO: At some point, try to make this work for files of unlimited size using a digest stream
            var reader = new FileReader();
            reader.onerror = function (err) { f(null); }
            reader.onload = function () { window.crypto.subtle.digest('SHA-384', reader.result).then(function (v) { f(arrayBufferToHex(v)); }, function () { f(null); }); };
            reader.readAsArrayBuffer(file);
        }

        // Push the next file
        function p13uploadNextFile() {
            uploadFile.xfilePtr++;
            if (uploadFile.xfiles.length > uploadFile.xfilePtr) {
                uploadFile.xptr = 0;
                var file = uploadFile.xfiles[uploadFile.xfilePtr];
                QH('p13dfileName', EscapeHtml(file.name));
                Q('d2progressBar').max = file.size;
                Q('d2progressBar').value = 0;
                if (file.xdata == null) {
                    uploadFile.xfile = file;
                    // If the remote file already exists and is smaller then our file, see if we can resume the trasfer
                    var f = null;
                    for (var i in p13filetree.dir) { if (p13filetree.dir[i].n == file.name) { f = p13filetree.dir[i]; } }
                    if ((f != null) && (f.s <= uploadFile.xfile.size)) {
                        performHashOnFile(uploadFile.xfile, function (hash) { files.sendText(JSON.stringify({ action: 'uploadhash', reqid: uploadFile.xfilePtr, path: uploadFile.xpath, name: file.name, tag: { h: hash.toUpperCase(), s: f.s, skip: f.s == uploadFile.xfile.size } })); });
                    } else {
                        files.sendText(JSON.stringify({ action: 'upload', reqid: uploadFile.xfilePtr, path: uploadFile.xpath, name: file.name, size: uploadFile.xfile.size }));
                    }
                } else {
                    // Data already loaded
                    uploadFile.xdata = file.xdata;
                    files.sendText(JSON.stringify({ action: 'upload', reqid: uploadFile.xfilePtr, path: uploadFile.xpath, name: file.name, size: uploadFile.xdata.byteLength }));
                }
            } else {
                p13uploadFileTransferDone();
            }
        }

        // Used to cancel the entire transfer.
        function p13uploadFileCancel(button, tag) {
            if (uploadFile != null) { files.sendText(JSON.stringify({ action: 'uploadcancel', reqid: uploadFile.xfilePtr })); uploadFile = null; }
            p13uploadFileTransferDone();
        }

        // Used to cancel the entire transfer.
        function p13uploadFileTransferDone() {
            uploadFile = null; // No more files to upload, clean up.
            setDialogMode(0); // Close the dialog box
            p13folderup(9999); // Refresh the current folder
        }

        // Receive upload ack from the mesh agent, use this to keep sending more data
        function p13gotUploadData(cmd) {
            if ((uploadFile == null) || (parseInt(uploadFile.xfilePtr) != parseInt(cmd.reqid))) { return; }
            switch (cmd.action) {
                case 'uploadstart': { uploadFile.xdataPriming = 8; p13uploadNextPart(false); break; } // Send 8 more blocks of 16k to fill the websocket.
                case 'uploadack': { p13uploadNextPart(false); break; }
                case 'uploaddone': { if (uploadFile.xfiles.length > uploadFile.xfilePtr + 1) { p13uploadNextFile(); } else { p13uploadFileTransferDone(); } break; }
                case 'uploaderror': { p13uploadFileCancel(); break; }
                case 'uploadhash': {
                    var file = uploadFile.xfiles[uploadFile.xfilePtr];
                    if (file) {
                        if (cmd.tag.h === cmd.hash) {
                            if (cmd.tag.skip) {
                                p13uploadNextFile();
                            } else {
                                uploadFile.xptr = cmd.tag.s;
                                files.sendText(JSON.stringify({ action: 'upload', reqid: uploadFile.xfilePtr, path: uploadFile.xpath, name: file.name, size: uploadFile.xfile.size, append: true }));
                            }
                        } else {
                            files.sendText(JSON.stringify({ action: 'upload', reqid: uploadFile.xfilePtr, path: uploadFile.xpath, name: file.name, size: uploadFile.xfile.size, append: false }));
                        }
                    }
                    break;
                }
            }
        }

        // Push the next part of the file into the websocket. If dataPriming is true, push more data only if it's not the last block of the file.
        function p13uploadNextPart(dataPriming) {
            if (uploadFile.xdata) {
                var data = uploadFile.xdata, start = uploadFile.xptr;
                if (start >= data.byteLength) {
                    files.sendText(JSON.stringify({ action: 'uploaddone', reqid: uploadFile.xfilePtr }));
                } else {
                    var end = uploadFile.xptr + (attemptWebRTC ? 16384 : 65536);
                    if (end > data.byteLength) { if (dataPriming == true) { return; } end = data.byteLength; }
                    var dataslice = new Uint8Array(data.slice(start, end))
                    if ((dataslice[0] == 123) || (dataslice[0] == 0)) {
                        var datapart = new Uint8Array(end - start + 1);
                        datapart.set(dataslice, 1); // Add a zero char at the start of the send, this will indicate that it's not a JSON command.
                        files.send(datapart);
                    } else {
                        files.send(dataslice); // The data does not start with 0 or 123 "{" so it can't be confused for JSON.
                    }
                    uploadFile.xptr = end;
                    Q('d2progressBar').value = end;
                }
            } else if (uploadFile.xfile) {
                if (uploadFile.xreader != null) return; // Data reading already in process
                if (uploadFile.xptr >= uploadFile.xfile.size) return;
                var end = uploadFile.xptr + (attemptWebRTC ? 16384 : 65536);
                if (end > uploadFile.xfile.size) { if (dataPriming == true) { return; } end = uploadFile.xfile.size; }
                uploadFile.xreader = new FileReader();
                uploadFile.xreader.onerror = function (err) { console.log(err); }
                uploadFile.xreader.onload = function () {
                    var data = uploadFile.xreader.result;
                    delete uploadFile.xreader;
                    if (data == null) return;
                    var dataslice = new Uint8Array(data)
                    if ((dataslice[0] == 123) || (dataslice[0] == 0)) {
                        var datapart = new Uint8Array(data.byteLength + 1);
                        datapart.set(dataslice, 1); // Add a zero char at the start of the send, this will indicate that it's not a JSON command.
                        files.send(datapart);
                    } else {
                        files.send(dataslice); // The data does not start with 0 or 123 "{" so it can't be confused for JSON.
                    }
                    uploadFile.xptr = end;
                    Q('d2progressBar').value = end;
                    if (uploadFile.xptr >= uploadFile.xfile.size) {
                        files.sendText(JSON.stringify({ action: 'uploaddone', reqid: uploadFile.xfilePtr }));
                    } else {
                        if (uploadFile.xdataPriming > 0) { uploadFile.xdataPriming--; p13uploadNextPart(true); }
                    }
                };
                uploadFile.xreader.readAsArrayBuffer(uploadFile.xfile.slice(uploadFile.xptr, end));
            }
        }

        //
        // DEVICE EVENTS
        //

        var currentDeviceEvents = null;
        function deviceEventsUpdate() {
            var x = '', dateHeader = null;
            for (var i in currentDeviceEvents) {
                var event = currentDeviceEvents[i], time = new Date(event.time);
                if (event.msg) {
                    if (event.h == null) { event.h = Math.random(); }
                    if (printDate(time) != dateHeader) {
                        if (dateHeader != null) x += '</table>';
                        dateHeader = printDate(time);
                        x += '<table class="table table-hover p3eventsTable" cellpadding=0 cellspacing=0><tr><td colspan=4 class=DevSt>' + dateHeader + '</td></tr>';
                    }
                    var icon = 'fa-mobile-screen';
                    if (event.etype == 'user') icon = 'fa-user';
                    if (event.etype == 'server') icon = 'fa-server';

                    var msg;
                    if ((event.msgid == null) || (eventsMessageId[event.msgid] == null)) {
                        if (typeof event.msg == 'string') { msg = EscapeHtml(event.msg).split('(R)').join('&reg;'); } else { msg = ''; }
                    } else {
                        msg = eventsMessageId[event.msgid];
                        for (var i in event.msgArgs) {
                            var xx = event.msgArgs[i];
                            if ((typeof xx == 'string') && (xx.indexOf('DATETIME:') == 0)) { xx = printFlexDateTime(new Date(parseInt(xx.substring(9)))); }
                            msg = msg.split('{' + i + '}').join(xx);
                        }
                        //if (event.msgArgs != null) { for (var i in event.msgArgs) { msg = msg.split('{' + i + '}').join(event.msgArgs[i]); } }
                        msg = EscapeHtml(msg).split('(R)').join('&reg;');
                    }
                    if (event.username) {
                        var guestname = '';
                        if (event.guestname) { guestname = ' / <span title="' + "Esta es una sesión para compartir con invitados" + '">' + EscapeHtml(event.guestname) + '</span>'; }
                        if ((userinfo.siteadmin & 2) && (event.userid)) {
                            msg = '<a href=# onclick=\'gotoUser("' + encodeURIComponentEx(event.userid) + '");haltEvent(event);\'>' + EscapeHtml(event.username) + '</a>' + guestname + ' &rarr; ' + msg;
                        } else {
                            msg = EscapeHtml(event.username) + guestname + ' &rarr; ' + msg;
                        }
                    }
                    if (event.etype == 'relay' || event.action == 'relaylog') icon = 'fa-arrow-right-arrow-left';
                    x += '<tr onclick=showEventDetails(' + event.h + ',1) onmouseover=eventMouseHover(this,1) onmouseout=eventMouseHover(this,0) role="button"><td style=width:18px><i class="fa-solid ' + icon + '"></i></td><td class=g1>&nbsp;</td><td class=style10>' + printTime(time) + ' - ' + msg + '</td></tr><tr></tr>';
                }
            }
            if (dateHeader != null) x += '</table>';
            if (x == '') x = '<br><i>' + "No se Encontraron Eventos" + '</i><br><br>';
            QH('p16events', x);
        }

        function refreshDeviceEvents() {
            if (p16filterevents.value != "") {
                meshserver.send({ action: 'events', nodeid: currentNode._id, limit: parseInt(p16limitdropdown.value), filter: p16filterevents.value });
            } else {
                meshserver.send({ action: 'events', nodeid: currentNode._id, limit: parseInt(p16limitdropdown.value) });
            }
        }

        function showEventDetails(h, mode) {
            if (xxdialogMode) return false;
            var eventList, xevent;
            if (mode == 1) { eventList = currentDeviceEvents; }
            if (mode == 2) { eventList = events; }
            if (mode == 3) { eventList = currentUserEvents; }
            for (var i in eventList) { if (eventList[i].h == h) { xevent = eventList[i]; break; } }
            if (xevent) {
                var x = '<div style=overflow-y:auto;max-height:300px>';
                for (var i in xevent) {
                    if ((i == 'h') || (i == '_id') || (i == 'ids') || (i == 'domain') || (xevent[i] == null)) continue;
                    if (typeof xevent[i] == 'object') {
                        x += addHtmlValue3(EscapeHtml(i), EscapeHtml(JSON.stringify(xevent[i])));
                    } else {
                        x += addHtmlValue3(EscapeHtml(i), EscapeHtml(xevent[i]));
                    }
                }
                x += '</div>';

                setModalContent('xxAddAgent', "Detalles del Evento", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
        }

        //
        // DEVICE DETAILS
        //

        var deviceDetailsStats = null;
        var deviceDetailsStatsTimer = null;
        var deviceDetailsStatsData = [];
        var deviceDetailsConfig = {
            type: 'line',
            data: {
                labels: [],
                datasets: [
                    { label: "CPU", backgroundColor: 'rgba(134, 16, 158, .5)', borderColor: 'rgb(134, 16, 158)', data: [], fill: true },
                    { label: "Memoria", backgroundColor: 'rgba(255, 99, 132, .5)', borderColor: 'rgb(255, 99, 132)', data: [], fill: true }
                ]
            },
            options: {
                events: ['click'],
                animation: false,
                responsive: true,
                maintainAspectRatio: false,
                tooltips: { enabled: false },
                elements: { line: { cubicInterpolationMode: 'monotone' } },
                scales: {
                    x: { type: 'linear', display: true, title: { display: false, text: '' }, min: 0, max: 100, reverse: true },
                    y: { type: 'linear', display: true, title: { display: false, text: '' }, min: 0, max: 100 }
                }
            }
        };

        function deskToggleCpuGraph() {
            var visible = (QS('p17graph').display == 'none');
            if (visible) {
                QV('p17graph', true);
                window.deviceDetailsStatsChart = new Chart(document.getElementById('deviceDetailsStats').getContext('2d'), deviceDetailsConfig);
                deviceDetailsStatsTimer = setInterval(deviceDetailsStatsTimerFunc, 2000);
                deviceDetailsStatsTimerFunc();
            } else deviceDetailsStatsClear();
        }

        function refreshDetails() {
            meshserver.send({ action: 'msg', type: 'sysinfo', nodeid: currentNode._id });
        }

        function deviceDetailsStatsClear() {
            QV('p17graph', false);
            if (window.deviceDetailsStatsChart != null) { window.deviceDetailsStatsChart.destroy(); delete window.deviceDetailsStatsChart; }
            if (deviceDetailsStatsTimer != null) { clearInterval(deviceDetailsStatsTimer); deviceDetailsStatsTimer = null; }
            deviceDetailsStatsData = [];
            deviceDetailsStatsDraw();
            QV('extraGraphValues', false);
            QH('extraGraphValues', '');
        }

        function deviceDetailsStatsTimerFunc() {
            if (currentNode != null) { meshserver.send({ action: 'msg', type: 'cpuinfo', nodeid: currentNode._id }); }
        }

        function deviceDetailsStatsDraw(message) {
            var now = Date.now() / 1000, oldLimit = (now - 100);
            while ((deviceDetailsStatsData.length > 0) && (deviceDetailsStatsData[0][0] < (oldLimit - 5))) { deviceDetailsStatsData.shift(); }

            var cpu = [], memory = [];
            for (var i in deviceDetailsStatsData) {
                var d = deviceDetailsStatsData[i];
                cpu.push({ x: 100 - (d[0] - oldLimit), y: d[1] });
                memory.push({ x: 100 - (d[0] - oldLimit), y: d[2] });
            }

            deviceDetailsConfig.data.datasets[0].data = cpu;
            deviceDetailsConfig.data.datasets[1].data = memory;
            if (window.deviceDetailsStatsChart) { window.deviceDetailsStatsChart.update(); }

            // Display any additional live values
            if (message != null) {
                if (Array.isArray(message.thermals) && (message.thermals.length > 0)) {
                    var x = '&nbsp;';
                    for (var i in message.thermals) { x += '<div class=thermalSensor title="' + EscapeHtml(message.thermals[i].InstanceName) + '">' + parseFloat(message.thermals[i].CurrentTemperature).toFixed(2) + '&deg;C / ' + parseFloat((message.thermals[i].CurrentTemperature * 1.8) + 32).toFixed(2) + '&deg;F' + '</div>'; }
                    QV('extraGraphValues', true);
                    QH('extraGraphValues', x);
                }
            }
        }

        var DeviceDetailsHardware = null;
        var DeviceDetailsNetwork = null;
        var DeviceDetailsNodeId = null;
        function updateDeviceDetails(node, hardware, network) {
            if (currentNode == null) return;
            if (node == null) { node = currentNode; }
            if (currentNode._id != node._id) return;
            if (DeviceDetailsNodeId != node._id) { DeviceDetailsHardware = null; DeviceDetailsNetwork = null; DeviceDetailsNodeId = node._id; }
            if (hardware != null) { DeviceDetailsHardware = hardware; }
            if (network != null) { DeviceDetailsNetwork = network; }
            hardware = DeviceDetailsHardware;
            network = DeviceDetailsNetwork;
            if (hardware == null) { hardware = {}; }
            if (network == null) { network = {}; }
            var sections = [], s = {};

            // Operating System
            var x = '';
            if (node.rname) { x += addDetailItem("Nombre", EscapeHtml(node.rname), s); }
            if (hardware.windows && hardware.windows.osinfo && hardware.windows.osinfo.Description) { x += addDetailItem("Descripción", EscapeHtml(hardware.windows.osinfo.Description), s); }
            if (node.osdesc) { x += addDetailItem("Versión", EscapeHtml(node.osdesc), s); }
            if (hardware.windows && hardware.windows.osinfo) {
                var m = hardware.windows.osinfo;
				if(m.BuildRevision){ x += addDetailItem("Número de compilación", EscapeHtml(m.BuildRevision), s); }
                if (m.OSArchitecture) {
                    if (m.OSArchitecture.startsWith('32')) { x += addDetailItem("Arquitectura", "32 bits", s); }
                    else if (m.OSArchitecture.startsWith('64')) { x += addDetailItem("Arquitectura", "64 bits", s); }
                    else { x += addDetailItem("Arquitectura", EscapeHtml(m.OSArchitecture), s); }
                }
                if (m.LastBootUpTime) {
                    var thedate = {
                        year: parseInt(m.LastBootUpTime.substring(0, 4)),
                        month: parseInt(m.LastBootUpTime.substring(4, 6)) - 1, // Months are 0-based in JavaScript (0 - January, 11 - December)
                        day: parseInt(m.LastBootUpTime.substring(6, 8)),
                        hours: parseInt(m.LastBootUpTime.substring(8, 10)),
                        minutes: parseInt(m.LastBootUpTime.substring(10, 12)),
                        seconds: parseInt(m.LastBootUpTime.substring(12, 14)),
                    };
                    const date = printDateTime(new Date(thedate.year, thedate.month, thedate.day, thedate.hours, thedate.minutes, thedate.seconds));
                    x += addDetailItem("Last Boot Up Time", date);
                }
                if(m.Domain){ x += addDetailItem((m.PartOfDomain ? "Dominio" : "Grupo de trabajo"), EscapeHtml(m.Domain), s); }
                if(m.DomainState > 0){ x += addDetailItem("Estado del dominio", (domainStates[m.DomainState] ? domainStates[m.DomainState] : "Desconocido"), s); }
            }
            if (hardware.linux && hardware.linux.LastBootUpTime) {
                var lastBootUpTime = new Date(hardware.linux.LastBootUpTime);
                const date = printDateTime(lastBootUpTime);
                x += addDetailItem("Last Boot Up Time", date);
            }
            if (hardware.linux && hardware.linux.arch) {
                var arch = hardware.linux.arch;
                x += addDetailItem("Arquitectura", (['x86_64','amd64'].includes(arch) ? "64 bits" : ['i686','x86','i586'].includes(arch) ? "32 bits" : EscapeHtml(arch)), s);
            }
            if (hardware.linux && hardware.linux.kernel_release) { x += addDetailItem("Versión del núcleo", EscapeHtml(hardware.linux.kernel_release), s); }
            if (hardware.linux && hardware.linux.kernel_build) { x += addDetailItem("Compilación del núcleo", EscapeHtml(hardware.linux.kernel_build), s); }
            if (hardware.darwin && hardware.darwin.LastBootUpTime) {
                var lastBootUpTime = new Date(hardware.darwin.LastBootUpTime * 1000); // must times by 1000 even tho timestamp is correct?
                const date = printDateTime(lastBootUpTime);
                x += addDetailItem("Last Boot Up Time", date);
            }
            if (x != '') { sections.push({ name: "Sistema Operativo", html: x, img: 'software64.png' }); }

            // MeshAgent
            if (node.agent) {
                var x = '';
                if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
                    var str = '';
                    if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
                    if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
                    if (node.agent.id == 14) { str = node.agent.core; }
                    x += addDetailItem("Agente Mesh", str);
                }
                if (node.firstconnect) { x += addDetailItem("Conexión del primer agente", printDateTime(new Date(node.firstconnect))); }
                if ((node.conn & 1) != 0) {
                    x += addDetailItem("Última conexión del agente", "Conectado ahora");
                } else {
                    if (node.lastconnect) { x += addDetailItem("Última conexión del agente", printDateTime(new Date(node.lastconnect))); }
                }
                if (node.lastaddr) {
                    var splitip = node.lastaddr.split(':');
                    if (splitip.length > 2) {
                        // IPv6
                        x += addDetailItem("Última dirección IP del agente", '<a href="https://iplocation.com/?ip=' + splitip.slice(0, -1).join(':') + '" rel="noreferrer noopener" target="MeshIPLoopup">' + splitip.slice(0, -1).join(':') + '</a>');
                    } else {
                        // IPv4
                        if (isPrivateIP(node.lastaddr)) {
                            x += addDetailItem("Última dirección IP del agente", splitip[0]);
                        } else {
                            x += addDetailItem("Última dirección IP del agente", '<a href="https://iplocation.com/?ip=' + splitip[0] + '" rel="noreferrer noopener" target="MeshIPLoopup">' + splitip[0] + '</a>');
                        }
                    }
                }
                if (hardware.agentvers != null) {
                    if (hardware.agentvers.compileTime) {
                        var d = Date.parse(hardware.agentvers.compileTime)
                        x += addDetailItem("Fecha de compilación", isNaN(d) ? EscapeHtml(hardware.agentvers.compileTime) : printDateTime(new Date(d)));
                    }
                }
                if (hardware.time != null) {
                    x += addDetailItem("Última actualización de detalles", printDateTime(new Date(hardware.time)));
                }
                if (x != '') { sections.push({ name: "Agente Mesh", html: x, img: 'meshagent64.png' }); }
            }

            // Mobile
            if (hardware.mobile) {
                var x = '';
                if (hardware.mobile.brand && hardware.mobile.model) { x += addDetailItem("Modelo", EscapeHtml(hardware.mobile.brand + ', ' + hardware.mobile.model), s); }
                if (hardware.mobile.device) { x += addDetailItem("Dispositivo", EscapeHtml(hardware.mobile.device), s); }
                if (hardware.mobile.bootloader) { x += addDetailItem("Bootloader", EscapeHtml(hardware.mobile.bootloader), s); }
                if (hardware.mobile.id) { x += addDetailItem("Identificador", EscapeHtml(hardware.mobile.id), s); }
                if (hardware.mobile.host) { x += addDetailItem("Nombre de Host", EscapeHtml(hardware.mobile.host), s); }
                if (hardware.mobile.androidapi && hardware.mobile.androidrelease) { x += addDetailItem("Android Version", EscapeHtml(hardware.mobile.androidrelease + ', API Level ' + hardware.mobile.androidapi), s); }
                if (x != '') { sections.push({ name: "Dispositivo móvil", html: x, img: 'mobile64.png' }); }
            }

            // Networking (old style)
            if (network.netif != null) {
                // Compact interfaces that have the same MAC addresses
                var macInterfaces = {}
                for (var i in network.netif) {
                    var m = network.netif[i];
                    if (typeof m.mac != 'string') continue;
                    if (macInterfaces[m.mac] == null) {
                        macInterfaces[m.mac] = m;
                        macInterfaces[m.mac].ipv4layer = [{ v4addr: m.v4addr, v4mask: m.v4mask, v4gateway: m.v4gateway }];
                    } else {
                        macInterfaces[m.mac].ipv4layer.push({ v4addr: m.v4addr, v4mask: m.v4mask, v4gateway: m.v4gateway });
                    }
                }

                // Display one network interface for each MAC address
                var x = '';
                x += '<table class="table table-hover" style=width:100%>';
                for (var i in macInterfaces) {
                    var m = macInterfaces[i];
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + EscapeHtml(m.name + (m.dnssuffix ? (', ' + m.dnssuffix) : '')) + '</b></div>';
                    if (m.desc) { x += addDetailItem("Descripción", EscapeHtml(m.desc).split('(R)').join('&reg;')); }
                    //if (m.dnssuffix) { x += addDetailItem("DNS Suffix", m.dnssuffix); }
                    if (m.mac) {
                        if (m.gatewaymac) {
                            x += addDetailItem("Capa MAC", format("MAC: {0}, Puerta de Enlace: {1}", EscapeHtml(m.mac), EscapeHtml(m.gatewaymac)));
                        } else {
                            if (m.mac) { x += addDetailItem("Capa MAC", format("MAC: {0}", EscapeHtml(m.mac))); }
                        }
                    }
                    for (var j in m.ipv4layer) {
                        var ipv4layer = m.ipv4layer[j];
                        if (ipv4layer.v4gateway && ipv4layer.v4mask) {
                            x += addDetailItem("Capa IPv4", format("IP: {0}, Máscara: {1}, Puerta de enlace: {2}", EscapeHtml(ipv4layer.v4addr), EscapeHtml(ipv4layer.v4mask), EscapeHtml(ipv4layer.v4gateway)));
                        } else {
                            if (ipv4layer.v4addr) { x += addDetailItem("Capa IPv4", format("IP: {0}", EscapeHtml(ipv4layer.v4addr))); }
                        }
                    }
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Redes", html: x, img: 'networking64.png' }); }
            }

            // Networking
            if (network.netif2 != null) {
                // Display one network interface for each MAC address
                var x = '';
                x += '<table class="table table-hover" style=width:100%>';
                for (var i in network.netif2) {
                    var m = network.netif2[i];
                    if ((Array.isArray(m) == false) || (m.length < 1) || (m[0] == null) || ((typeof m[0].mac == 'string') && (m[0].mac.startsWith('00:00:00:00')))) continue;
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    var ifTitle = '';
                    if (i != null) ifTitle += i;
                    if (m[0].fqdn != null && m[0].fqdn != '') ifTitle += ', ' + m[0].fqdn;
                    if (ifTitle.length > 0) { x += '<div style=margin-bottom:3px><b>' + EscapeHtml(ifTitle) + '</b></div>'; }
                    if (m.desc) { x += addDetailItem("Descripción", EscapeHtml(m.desc).split('(R)').join('&reg;')); }
                    //if (m.dnssuffix) { x += addDetailItem("DNS Suffix", m.dnssuffix); }
                    if (typeof m[0].mac == 'string') {
                        if (m[0].gatewaymac) {
                            x += addDetailItem("Capa MAC", format("MAC: {0}, Puerta de Enlace: {1}", EscapeHtml(m[0].mac), EscapeHtml(m[0].gatewaymac)));
                        } else {
                            x += addDetailItem("Capa MAC", format("MAC: {0}", EscapeHtml(m[0].mac)));
                        }
                    }
                    if (typeof m[0].speed == 'number' && (m[0].speed != 9223372036854775807 && m[0].speed > 0)) {
                        x += addDetailItem("Velocidad de la interfaz", format("{0}", getNetworkSpeed(m[0].speed)));
                    }
                    for (var j = 0; j < m.length; j++) {
                        var iplayer = m[j], items = [];
                        if (iplayer.address) { items.push(format("IP: {0}", EscapeHtml(iplayer.address))); }
                        if (iplayer.netmask) { items.push(format("Máscara: {0}", EscapeHtml(iplayer.netmask))); }
                        if (iplayer.gateway) { items.push(format("Puerta de enlace: {0}", EscapeHtml(iplayer.gateway))); }
                        if (items.length > 0) {
                            if (iplayer.family == 'IPv4') { x += addDetailItem("Capa IPv4", items.join(", ")); }
                            if (iplayer.family == 'IPv6') { x += addDetailItem("Capa IPv6", items.join(", ")); }
                        }
                    }
                    x += '</div></td></tr>';
                }
                if (hardware.network && hardware.network.dns) {
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += addDetailItem('<b>' + "DNS Servers" + '</b>', EscapeHtml(hardware.network.dns.join(", ")));
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Redes", html: x, img: 'networking64.png' }); }
            }

            // Attribute: Intel AMT
            if (node.intelamt != null) {
                var x = '';
                x += addDetailItem("Versión", (node.intelamt.ver) ? ('v' + EscapeHtml(node.intelamt.ver)) : ('<i>' + "Desconocido" + '</i>'), s);
                var provisioningStates = { 0: nobreak("No Activada"), 1: nobreak("Preactivación"), 2: nobreak("Activado") };
                var provisioningMode = '';
                if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Modo de Control de Cliente (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Modo de Control de Administrador (ACM)"); } }
                x += addDetailItem("Estado de Aprovisionamiento", ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('<i>' + "Desconocido" + '</i>')) + provisioningMode, s);
                x += addDetailItem("Seguridad", (node.intelamt.tls == 1) ? "Asergurado usando TLS" : "TLS no esta configurado", s);
                // Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
                x += addDetailItem("Credenciales del Administrador", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0))) ? "No Conocido" : "Conocido", s);
                if (x != '') {
                    if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
                        sections.push({ name: "Capacidad de gestión estándar de Intel&reg; (Intel&reg; SM)", html: x, img: 'amt64.png' });
                    } else {
                        sections.push({ name: "Intel&reg; Active Management Technology (Intel&reg; AMT)", html: x, img: 'amt64.png' });
                    }
                }
            }

            if (hardware.identifiers) {
                var x = '', ident = hardware.identifiers;
                // BIOS
                if (ident.bios_vendor) { x += addDetailItem("Fabricante", EscapeHtml(ident.bios_vendor), s); }
                if (ident.bios_version) { x += addDetailItem("Versión", EscapeHtml(ident.bios_version), s); }
                if (ident.bios_serial) { x += addDetailItem("Número de Serie", EscapeHtml(ident.bios_serial), s); }
                if (ident.bios_mode) { x += addDetailItem("Mode", EscapeHtml(ident.bios_mode), s); }
                if (x != '') { sections.push({ name: "BIOS", html: x, img: 'chip64.png' }); }

                // Motherboard
                x = '';
                if (ident.board_vendor) { x += addDetailItem("Fabricante", EscapeHtml(ident.board_vendor), s); }
                if (ident.board_name) { x += addDetailItem("Nombre", EscapeHtml(ident.board_name), s); }
                if (ident.board_serial && (ident.board_serial != '')) { x += addDetailItem("Número de Serie", EscapeHtml(ident.board_serial), s); }
                if (ident.board_version) { x += addDetailItem("Versión", EscapeHtml(ident.board_version), s); }
                if (ident.product_uuid) { x += addDetailItem("Identificador", EscapeHtml(ident.product_uuid), s); }
                if (ident.cpu_name) { x += addDetailItem("CPU", EscapeHtml(ident.cpu_name).split('(TM)').join('&trade;').split('(R)').join('&reg;'), s); }
                if (ident.gpu_name) { for (var i in ident.gpu_name) { x += addDetailItem("GPU", EscapeHtml(ident.gpu_name[i]).split('(TM)').join('&trade;').split('(R)').join('&reg;'), s); } }
                if (x != '') { sections.push({ name: "Placa Madre", html: x, img: 'motherboard64.png' }); }

                // System
                x = '';
                if (ident.chassis_manufacturer) { x += addDetailItem("Fabricante", EscapeHtml(ident.chassis_manufacturer), s); }
                if (ident.product_name) { x += addDetailItem("Nombre del Producto", EscapeHtml(ident.product_name), s); }
                if (ident.chassis_serial) { x += addDetailItem("Número de Serie", EscapeHtml(ident.chassis_serial), s); }
                if (ident.chassis_assettag) { x += addDetailItem("Etiqueta de activo", EscapeHtml(ident.chassis_assettag), s); }
                if (x != '') { sections.push({ name: "Sistema", html: x, img: 'system64.png' }); }
            }

            // TPM
            if (hardware.tpm) {
                var x = '', tpm = hardware.tpm;
                if (tpm.SpecVersion) { x += addDetailItem("SpecVersion", parseFloat(EscapeHtml(tpm.SpecVersion)).toFixed(1), s); }
                if (tpm.ManufacturerId) { x += addDetailItem("Identificador", EscapeHtml(tpm.ManufacturerId), s); }
                if (tpm.ManufacturerVersion) { x += addDetailItem("Versión", EscapeHtml(tpm.ManufacturerVersion), s); }
                if (tpm.IsActivated != null) { x += addDetailItem("Activado", (tpm.IsActivated ? "Yes" : "No"), s); }
                if (tpm.IsEnabled != null) { x += addDetailItem("Habilitado", (tpm.IsEnabled ? "Yes" : "No"), s); }
                if (tpm.IsOwned != null) { x += addDetailItem("Propiedad", (tpm.IsOwned ? "Yes" : "No"), s); }
                if (x != '') { sections.push({ name: "TPM", html: x, img: 'tpm64.png' }); }
            }

            if (hardware.battery) {
                var x = '';
                x += '<table class="table table-hover" style=width:100%>';
                for (var i in hardware.battery) {
                    var battery = hardware.battery[i];
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + EscapeHtml(battery.DeviceName ? battery.DeviceName : "Desconocido") + '</b></div>';
                    if (battery.CycleCount) { x += addDetailItem("Conteo de ciclos", EscapeHtml(battery.CycleCount), s); }
                    if (battery.FullChargedCapacity) { x += addDetailItem("Capacidad de carga completa", format("{0} mWh", battery.FullChargedCapacity), s); }
                    if (battery.EstimatedRuntime) { x += addDetailItem("Tiempo de ejecución estimado", format("{0} minutos", Math.floor((battery.EstimatedRuntime / 60))), s); }
                    if (battery.Chemistry) { x += addDetailItem("Química", EscapeHtml(battery.Chemistry), s); }
                    if (battery.DesignedCapacity) { x += addDetailItem("Capacidad de diseño", format("{0} mWh", battery.DesignedCapacity), s); }
                    if (battery.ManufactureDate) { x += addDetailItem("Fecha de fabricación", EscapeHtml(battery.ManufactureDate), s); }
                    if (battery.ManufactureName) { x += addDetailItem("Nombre de fabricación", EscapeHtml(battery.ManufactureName), s); }
                    if (battery.SerialNumber) { x += addDetailItem("Número de serie", EscapeHtml(battery.SerialNumber), s); }
                    if (battery.ChargeRate) { x += addDetailItem("Tasa de carga", format("{0} mW", battery.ChargeRate), s); }
                    if (battery.Charging != null) { x += addDetailItem("Cargando", (battery.Charging ? "Yes" : "No"), s); }
                    if (battery.DischargeRate) { x += addDetailItem("Tasa de descarga", format("{0} mW", battery.DischargeRate), s); }
                    if (battery.Discharging != null) { x += addDetailItem("Descarga", (battery.Discharging ? "Yes" : "No"), s); }
                    if (battery.RemainingCapacity) { x += addDetailItem("Capacidad restante", format("{0} mWh", battery.RemainingCapacity), s); }
                    if (battery.Voltage) { x += addDetailItem("Voltaje", format("{0} V", (battery.Voltage / 1000)), s); }
                    if (battery.Health) { x += addDetailItem("Salud", format("{0} %", battery.Health), s); }
                    if (battery.BatteryCharge) { x += addDetailItem("Carga de batería", format("{0} %", battery.BatteryCharge), s); }
                    x += '</div>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Batería", html: x, img: 'battery64.png'}); }
            }

            if (hardware.windows) {
                if (hardware.windows.memory && (hardware.windows.memory.length > 0)) {
                    var x = '';
                    // Sort Memory
                    hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; });

                    x += '<table class="table table-hover" style=width:100%>';
                    for (var i in hardware.windows.memory) {
                        var m = hardware.windows.memory[i];
                        x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                        x += '<div style=margin-bottom:3px><b>' + EscapeHtml((m.BankLabel ? m.BankLabel : (m.DeviceLocator ? m.DeviceLocator : 'Unknown'))) + '</b></div>';
                        if (m.Capacity && m.Speed) { x += addDetailItem("Capacidad / Velocidad", EscapeHtml(format("{0} Mb, {1} Mhz", (m.Capacity / 1024 / 1024), m.Speed)), s); }
                        else if (m.Capacity) { x += addDetailItem("Capacidad", EscapeHtml(format("{0} Mb", (m.Capacity / 1024 / 1024))), s); }
                        if (m.PartNumber) { x += addDetailItem("Numero de Parte", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Memoria", html: x, img: 'ram64.png' }); }
                }
            }

            if (hardware.linux) {
                if (hardware.linux.memory && (hardware.linux.memory.Memory_Device.length > 0)) {
                    var x = '';
                    // Sort Memory
                    hardware.linux.memory.Memory_Device.sort(function (a, b) { if (a.Locator > b.Locator) return 1; if (a.Locator < b.Locator) return -1; return 0; });

                    x += '<table class="table table-hover" style=width:100%>';
                    for (var i in hardware.linux.memory.Memory_Device) {
                        var m = hardware.linux.memory.Memory_Device[i];
                        if (m.Size && (m.Size == 'No Module Installed')) continue;
                        x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                        x += '<div style=margin-bottom:3px><b>' + EscapeHtml((m.Locator ? m.Locator : 'Unknown')) + '</b></div>';
                        if (m.Size && m.Speed) { x += addDetailItem("Capacidad / Velocidad", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Capacidad", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Numero de Parte", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Memoria", html: x, img: 'ram64.png' }); }
                }
            }

            if (hardware.darwin) {
                if (hardware.darwin.memory && (hardware.darwin.memory.length > 0)) {
                    var x = '';
                    x += '<table class="table table-hover" style=width:100%>';
                    for (var i in hardware.darwin.memory) {
                        var m = hardware.darwin.memory[i];
                        if (m.Size && (m.Size == 'No Module Installed')) continue;
                        x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                        x += '<div style=margin-bottom:3px><b>' + EscapeHtml((m.DeviceLocator ? m.DeviceLocator : 'Unknown')) + '</b></div>';
                        if (m.Size && m.Speed) { x += addDetailItem("Capacidad / Velocidad", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Capacidad", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Numero de Parte", EscapeHtml((m.Manufacturer && m.Manufacturer != '') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';
                    if (x != '') { sections.push({ name: "Memoria", html: x, img: 'ram64.png' }); }
                }
            }

            // Storage
            if (hardware.identifiers && hardware.identifiers.storage_devices) {
                var x = '';
                // Sort Storage
                ident.storage_devices.sort(function (a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });

                x += '<table style=width:100%>';
                for (var i in ident.storage_devices) {
                    var m = ident.storage_devices[i];
                    if (m.Size) {
                        x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                        x += '<div style=margin-bottom:3px><b>' + EscapeHtml(m.Caption) + '</b></div>';
                        if (m.Model && (m.Model != m.Caption)) { x += addDetailItem("Modelo", EscapeHtml(m.Model), s); }
                        if (m.Size) {
                            if ((typeof m.Size == 'string') && (parseInt(m.Size) == m.Size)) { m.Size = parseInt(m.Size); }
                            if (typeof m.Size == 'number') { x += addDetailItem("Capacidad", format("{0} Mb", Math.floor(m.Size / 1024 / 1024)), s); }
                            if (typeof m.Size == 'string') { x += addDetailItem("Capacidad", EscapeHtml(m.Size), s); }
                        }
                        if (hardware.windows && hardware.windows.drives && m.Model) {
                            const foundObject = hardware.windows.drives.find(obj => obj['Model'] === m.Model);
                            if (foundObject && foundObject.Status) x += addDetailItem("Estado", EscapeHtml(foundObject.Status), s);
                        }
                        x += '</div>';
                    }
                }
                x += '</table>';

                if (x != '') { sections.push({ name: "Almacenamiento", html: x, img: 'storage64.png' }); }
            }

            // Volumes and Bitlocker
            if (hardware.windows && hardware.windows.volumes) {
                var x = '';
                const encMethod = { 0: '', 1: "AES-128 with diffuser", 2: "AES-256 with diffuser", 3: "AES-128", 4: "AES-256", 5: "Hardware encryption", 6: "XTS-AES-128", 7: "XTS-AES-256" };
                const driveType = { 0: "Desconocido", 1: "Sin directorio raíz", 2: "Disco extraíble", 3: "Disco local", 4: "Unidad de red", 5: "Disco compacto", 6: "Disco RAM" };
                const conversionStatus = { 1: "Totalmente cifrado", 2: "Cifrado en curso", 3: "Descifrado en curso", 4: "Cifrado en pausa", 5: "Descifrado en pausa" };
                for (var i in hardware.windows.volumes) {
                    var m = hardware.windows.volumes[i];
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + i + ':' + (((m.name == null) || (m.name == '')) ? '' : (' - ' + EscapeHtml(m.name))) + '</b></div>';
                    if (m.size) {
                        var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                        var j = parseInt(Math.floor(Math.log(Math.abs(m.size)) / Math.log(1024)), 10);
                        var fsize = (j === 0 ? `${m.size} ${sizes[j]}` : `${(m.size / (1024 ** j)).toFixed(2)} ${sizes[j]}`);
                        x += addDetailItem("Capacidad", EscapeHtml(fsize), s);
                    }
                    if (m.sizeremaining) {
                        var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                        var j = parseInt(Math.floor(Math.log(Math.abs(m.sizeremaining)) / Math.log(1024)), 10);
                        var fsize = (j === 0 ? `${m.sizeremaining} ${sizes[j]}` : `${(m.sizeremaining / (1024 ** j)).toFixed(2)} ${sizes[j]}`);
                        x += addDetailItem("Capacidad restante", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        if (m.dType != null) { type = driveType.hasOwnProperty(m.dType) ? driveType[m.dType] : "Desconocido"; }
                        x += addDetailItem("File System", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Desconocido" : EscapeHtml(m.type)), s);
                    }

                    if (m.protectionStatus || m.volumeStatus) { // volumeStatus=conversionStatus
                        var bitlockerState;
                        if (typeof(m.volumeStatus) === 'string') {  // string style db schema
                            bitlockerState = m.volumeStatus;
                        } else {    // new code db schema
                            if (m.protectionStatus == 2) {
                                bitlockerState = "Bloqueada";
                            } else {
                                bitlockerState = (conversionStatus.hasOwnProperty(m.volumeStatus) ? conversionStatus[m.volumeStatus] : "Desconocido");
                            }
                        }
                        bitlockerState += (encMethod.hasOwnProperty(m.encryptionMethod) ? ' / ' + encMethod[m.encryptionMethod] : ' / ' + "Desconocido");
                        var lastrp = (m.identifier && hardware.windows.bitlocker) ? hardware.windows.bitlocker[m.identifier] : null;
                        var legacyrp = (m.lastKnownRecoveryPassword && (m.lastKnownIdentifier === m.identifier)) ? m.lastKnownRecoveryPassword : null;   // pre key-list (un-migrated) docs
                        var sendPW = m.recoveryPassword || (lastrp ? lastrp.rp : null) || legacyrp || "External (PIN/Key/File)";
                        if (m.identifier) {
                            bitlockerState += addKeyLink('', 'deviceDetailsShowBitlockerInfo(\"' + encodeURIComponentEx(i) + '\",\"' + encodeURIComponentEx(m.identifier) + '\",\"' + encodeURIComponentEx(sendPW) + '\"' + ')');
                        }
                        x += addDetailItem("BitLocker", bitlockerState, s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Storage Volumes", html: '<table style=width:100%>' + x + '</table>', img: 'storage64.png' }); }
            }

            // Linux Volumes
            if (hardware.linux && hardware.linux.volumes) {
                var x = '';
                for (var i in hardware.linux.volumes) {
                    var m = hardware.linux.volumes[i];
                    if (m.mount_point.startsWith('/var/lib/docker/overlay2')) continue;
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + m.mount_point + '</b></div>';
                    if (m.size) {
                        var sizes = ['KB', 'MB', 'GB', 'TB'];
                        var j = parseInt(Math.floor(Math.log(Math.abs(m.size)) / Math.log(1024)), 10);
                        var fsize = (j === 0 ? `${m.size} ${sizes[j]}` : `${(m.size / (1024 ** j)).toFixed(2)} ${sizes[j]}`);
                        x += addDetailItem("Capacidad", EscapeHtml(fsize), s);
                    }
                    if (m.available) {
                        if (Math.abs(m.available) == 0) {
                            var fsize = `0 KB`;
                        } else {
                            var sizes = ['KB', 'MB', 'GB', 'TB'];
                            var j = parseInt(Math.floor(Math.log(Math.abs(m.available)) / Math.log(1024)), 10);
                            var fsize = (j === 0 ? `${m.available} ${sizes[j]}` : `${(m.available / (1024 ** j)).toFixed(2)} ${sizes[j]}`);
                        }
                        x += addDetailItem("Capacidad restante", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("File System", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Desconocido" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Storage Volumes", html: '<table style=width:100%>' + x + '</table>', img: 'storage64.png' }); }
            }

            // MacOS Volumes
            if (hardware.darwin && hardware.darwin.volumes) {
                var x = '';
                for (var i in hardware.darwin.volumes) {
                    var m = hardware.darwin.volumes[i];
                    if (m.mount_point.startsWith('/var/lib/docker/overlay2')) continue;
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + m.mount_point + '</b></div>';
                    if (m.size) {
                        x += addDetailItem("Capacidad", EscapeHtml(m.size), s);
                    }
                    if (m.available) {
                        x += addDetailItem("Capacidad restante", EscapeHtml(m.available), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("File System", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Desconocido" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Storage Volumes", html: '<table style=width:100%>' + x + '</table>', img: 'storage64.png' }); }
            }

            // Render the sections
            var x = '';
            for (var i in sections) {
                if (sections[i].img == null) {
                    x += '<div class=DevSt style=margin-bottom:3px;margin-left:16px><b>' + sections[i].name + '</b></div><div style=margin-bottom:10px;margin-left:16px>' + sections[i].html + '</div>';
                } else {
                    x += '<table style=width:100%><tr>';
                    x += '<td style=width:64px;vertical-align:top><img src=images/details/' + sections[i].img + ' border=0 width=64 /></td>'; // height=12
                    x += '<td><div class=DevSt style=margin-bottom:3px;margin-left:16px><b>' + sections[i].name + '</b></div><div style=margin-bottom:10px;margin-left:16px>' + sections[i].html + '</div></td>';
                    x += '</tr></table>';
                }
            }

            if (x == '') {
                QH('p17info2', "No hay información para este dispositivo.");
            } else {
                QH('p17info2', x);
            }
        }

        function deviceDetailsShowBitlockerInfo(drive, identifier, password) {
            if (xxdialogMode) return false;
            var x = '<div><p>' + "Identificador" + '</p><p style=user-select:text;font-weight:bold>' + (identifier ? decodeURIComponent(identifier) : "Desconocido") + '</p>';
            x += '<p>' + "Recovery Password" + '</p><p style=user-select:text;font-weight:bold>' + (password ? decodeURIComponent(password) : "Desconocido") + '</p></div>';
            setModalContent('xxAddAgent', decodeURIComponent(drive) + ': ' + "BitLocker Information", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

         // ============================================
        // INSTALLED SOFTWARE (Panel 18)
        // ============================================
        //

        var installedAppsData = { desktop: [], store: [] };
        var installedAppsLoading = false;
        var installedAppsLoadingType = 0;  // 0=idle, 1=desktop, 2=store
        var showStoreApps = false;

        function loadInstalledApps() {
            if (currentNode == null) return;
            if (installedAppsLoading) return;
            installedAppsLoading = true;
            installedAppsLoadingType = 1;  // Desktop Apps laden
            installedAppsData = { desktop: [], store: [] };  // Reset
            QH('p18Status', '<span>' + "Cargando aplicaciones de escritorio..." + '</span>');
            QH('p18html', '<div style="text-align:center;padding:50px;color:gray">' + "Cargando software instalado..." + '</div>');
            if ((currentNode.conn & 1) == 0) {
                QH('p18html', '<div style="text-align:center;padding:50px;color:#cc0000"><b>' + "El dispositivo está sin conexión." + '</b></div>');
                QH('p18Status', '<span>' + "Fuera de línea" + '</span>');
                installedAppsLoading = false;
                installedAppsLoadingType = 0;
                return;
            }
            meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'installedapps'});
        }

        function handleInstalledAppsResponse(data) {
            try {
                var apps = JSON.parse(data);
                if (!Array.isArray(apps)) { apps = []; }
                if (installedAppsLoadingType === 1) {
                    installedAppsData.desktop = apps;
                    installedAppsLoading = false;
                    installedAppsLoadingType = 0;
                    if (showStoreApps && currentNode != null) {
                        loadStoreApps();
                    } else {
                        displayInstalledApps();
                    }
                } else if (installedAppsLoadingType === 2) {
                    installedAppsData.store = apps;
                    installedAppsLoading = false;
                    installedAppsLoadingType = 0;
                    displayInstalledApps();
                } else {
                    installedAppsData.desktop = apps;
                    installedAppsLoading = false;
                    installedAppsLoadingType = 0;
                    displayInstalledApps();
                }
            } catch (e) {
                QH('p18Status', '<span>' + "ERROR: " + e.message + '</span>');
                installedAppsLoading = false;
                installedAppsLoadingType = 0;
            }
        }

        function loadStoreApps() {
            if (currentNode == null) return;
            if (installedAppsLoading) return;
            installedAppsLoading = true;
            installedAppsLoadingType = 2;
            QH('p18Status', '<span>' + "Cargando aplicaciones de la tienda..." + '</span>');
            meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'installedstoreapps' });
        }

        function toggleStoreApps() {
            showStoreApps = Q('p18ShowStore').checked;
            if (showStoreApps && installedAppsData.store.length === 0 && currentNode != null) {
                loadStoreApps();
            } else {
                displayInstalledApps();
            }
        }

        function displayInstalledApps() {
            var searchTerm = '';
            if (Q('p18SearchInput')) { searchTerm = Q('p18SearchInput').value.toLowerCase(); }
            var desktopApps = installedAppsData.desktop || [];
            var storeApps = installedAppsData.store || [];
            if (searchTerm) {
                desktopApps = desktopApps.filter(function(app) {
                    return (app.name && app.name.toLowerCase().indexOf(searchTerm) >= 0) ||
                        (app.publisher && app.publisher.toLowerCase().indexOf(searchTerm) >= 0);
                });
                storeApps = storeApps.filter(function(app) {
                    return (app.name && app.name.toLowerCase().indexOf(searchTerm) >= 0) ||
                        (app.publisher && app.publisher.toLowerCase().indexOf(searchTerm) >= 0);
                });
            }
            desktopApps.sort(function(a, b) { 
                return (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase()); 
            });
            storeApps.sort(function(a, b) { 
                return (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase()); 
            });
            var x = '';
            // ===== DESKTOP APPS TABLE (immer anzeigen) =====
            x += '<div style="margin-bottom:20px">';
            x += '<h4 style="border-bottom:2px solid var(--bs-primary);padding-bottom:5px;margin-top:0">';
            x += '<span style="color:var(--bs-primary)">&#128187;</span> ' + "Aplicaciones de escritorio" + ' (' + desktopApps.length + ')';
            x += '</h4>';
            x += '<table class="table table-striped table-bordered" style="width:100%;font-size:13px">';
            x += '<thead><tr>';
            x += '<th style="text-align:left">' + "Nombre" + '</th>';
            x += '<th style="text-align:left;width:120px">' + "Versión" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Editor" + '</th>';
            x += '<th style="text-align:left;width:100px">' + "Fecha de instalación" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Ubicación" + '</th>';
            x += '<th style="text-align:center;width:100px">' + "Acciones" + '</th>';
            x += '</tr></thead><tbody>';
            if (desktopApps.length === 0) {
                x += '<tr><td colspan="6" style="text-align:center;color:var(--bs-secondary);padding:20px">';
                x += searchTerm ? "No hay aplicaciones que coincidan" + '"' + EscapeHtml(searchTerm) + '"' : "No se encontraron aplicaciones de escritorio";
                x += '</td></tr>';
            } else {
                for (var i = 0; i < desktopApps.length; i++) {
                    var app = desktopApps[i];
                    var uninstallCmd = app.uninstall ? encodeURIComponent(app.uninstall) : '';
                    var hasUninstall = (uninstallCmd !== '');
                    x += '<tr>';
                    x += '<td title="' + EscapeHtml(app.name || '') + '">' + EscapeHtml(app.name || '-') + '</td>';
                    x += '<td>' + EscapeHtml(app.version || '-') + '</td>';
                    x += '<td>' + EscapeHtml(app.publisher || '-') + '</td>';
                    x += '<td>' + EscapeHtml(app.date || '-') + '</td>';
                    x += '<td>' + EscapeHtml(app.location || '-') + (app.arch ? ' (' + EscapeHtml(app.arch) + ')' : '') + '</td>';
                    x += '<td style="text-align:center">';
                    var userRights = GetNodeRights(currentNode);				
					if (hasUninstall && (userRights == 0xFFFFFFFF)) {
						x += '<button class="btn btn-sm btn-secondary" onclick="uninstallDesktopApp(\'' + encodeURIComponent(app.uninstall) + '\')">Uninstall</button>';
					} else {
						x += '<span style="color:var(--bs-secondary)">-</span>';
					}
                    x += '</td>';
                    x += '</tr>';
                }
            }
            x += '</tbody></table>';
            x += '</div>';
            // ===== STORE APPS TABLE (nur wenn Checkbox aktiv) =====
			if (showStoreApps) {
				x += '<div style="margin-bottom:20px">';
				x += '<h4 style="border-bottom:2px solid var(--bs-success);padding-bottom:5px">';
				x += '<span style="color:var(--bs-success)">&#127979;</span> ' + "Aplicaciones de Microsoft Store" + ' (' + storeApps.length + ')';
				x += '</h4>';
				x += '<table class="table table-striped table-bordered" style="width:100%;font-size:13px">';
				x += '<thead><tr>';
				x += '<th style="text-align:left">' + "Nombre" + '</th>';
				x += '<th style="text-align:left;width:120px">' + "Versión" + '</th>';
				x += '<th style="text-align:left;width:250px">' + "Paquete" + '</th>';
				x += '<th style="text-align:center;width:100px">' + "Ámbito" + '</th>';
				x += '<th style="text-align:center;width:80px">' + "Acciones" + '</th>';
				x += '</tr></thead><tbody>';
				if (storeApps.length === 0) {
					x += '<tr><td colspan="5" style="text-align:center;color:var(--bs-secondary);padding:20px">';
					x += searchTerm ? "No hay aplicaciones de la tienda que coincidan" + '"' + EscapeHtml(searchTerm) + '"' : "No se encontraron aplicaciones de la tienda";
					x += '</td></tr>';
				} else {
					for (var i = 0; i < storeApps.length; i++) {
						var app = storeApps[i];
						var uninstallCmd = app.uninstall ? encodeURIComponent(app.uninstall) : '';
						var hasUninstall = (uninstallCmd !== '');
						var scopeBadge = '';
						if (app.scope) {
							if (app.scope.indexOf('User') >= 0) {
								scopeBadge = '<span class="badge bg-warning" title="' + "Instalado solo para el usuario actual" + '">' + "Usuario" + '</span>';
							} else if (app.scope.indexOf('System') >= 0) {
								scopeBadge = '<span class="badge bg-info" title="' + "Instalado para todos los usuarios" + '">' + "Sistema" + '</span>';
							}
							if (app.scope.indexOf('Prov') >= 0) {
								scopeBadge += ' <span class="badge bg-secondary" title="' + "Aprovisionado para nuevos usuarios" + '">' + "Prov" + '</span>';
							}
						} else {
							scopeBadge = '<span>-</span>';
						}
						x += '<tr>';
						x += '<td>' + EscapeHtml(app.name || '-') + '</td>';
						x += '<td>' + EscapeHtml(app.version || '-') + '</td>';
						x += '<td title="' + EscapeHtml(app.packageFullName || '') + '">' + EscapeHtml(app.publisher || '-') + '</td>';
						x += '<td style="text-align:center">' + scopeBadge + '</td>';
						x += '<td style="text-align:center">';
						var userRights = GetNodeRights(currentNode);
						if (hasUninstall && (userRights == 0xFFFFFFFF)) {
							x += '<button class="btn btn-sm btn-secondary" onclick="uninstallStoreApp(\'' + encodeURIComponent(app.name) + '\')">Remove</button>';
						} else {
							x += '<span style="color:var(--bs-secondary)">-</span>';
						}
						x += '</td>';
						x += '</tr>';
					}
				}
				x += '</tbody></table>';
				x += '</div>';
			}
            QH('p18html', x);
            var totalDesktop = (installedAppsData.desktop || []).length;
            var totalStore = (installedAppsData.store || []).length;
            var shownDesktop = desktopApps.length;
            var shownStore = showStoreApps ? storeApps.length : 0;
            var statusText = "Escritorio:" + ' ' + shownDesktop + '/' + totalDesktop;
            if (showStoreApps) {
                statusText += ' | ' + "Tienda:" + ' ' + shownStore + '/' + totalStore;
            }
            QH('p18Status', statusText);
        }

        function filterInstalledApps() {
            if (filterInstalledApps.timer) clearTimeout(filterInstalledApps.timer);
            filterInstalledApps.timer = setTimeout(function() { displayInstalledApps(); }, 300);
        }

        function uninstallDesktopApp(encodedCmd) {
            if (xxdialogMode) return;
            if (currentNode == null) return;
            var cmd = decodeURIComponent(encodedCmd);
            if (!cmd || cmd.trim() === '') {
                QH('p18Status', '<span>' + "ERROR: " + "No hay comando de desinstalación disponible" + '</span>');
                return;
            }
            var dialogContent = '<div style="max-width:500px">';
            dialogContent += '<p><b>' + "¿Desinstalar este software?" + '</b></p>';
            dialogContent += '<div class="badge text-wrap w-100" style="background-color:var(--bs-secondary-bg);color:var(--bs-body-color);overflow-y:auto">' + EscapeHtml(cmd) + '</div>';
            dialogContent += '<p style="margin-top:10px"><i>' + "Se realizará una desinstalación silenciosa" + '</i></p>';
            dialogContent += '</div>';
            setModalContent('xxAddAgent', "Desinstalar software", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
                QH('p18Status', '<span>' + "Desinstalando..." + '</span>');
                var base64Cmd = btoa(unescape(encodeURIComponent(cmd))); //Use Base64 encoding to avoid special character problems.
                meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'uninstallapp', value: base64Cmd });
                setTimeout(function() { QH('p18Status', "Desinstalación iniciada. Haga clic en Actualizar para actualizar"); }, 2000);
            });
        }

        function uninstallStoreApp(encodedCmd) {
			if (xxdialogMode) return;
			if (currentNode == null) return;
			var cmd = decodeURIComponent(encodedCmd);
			var dialogContent = '<div style="max-width:500px">';
			dialogContent += '<p><b>' + "¿Eliminar esta aplicación de la tienda?" + '</b></p>';
			dialogContent += '<div class="badge text-wrap w-100" style="background-color:var(--bs-secondary-bg);color:var(--bs-body-color);overflow-y:auto">' + EscapeHtml(cmd) + '</div>';
			dialogContent += '</div>';
            setModalContent('xxAddAgent', "Eliminar aplicación de la tienda", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
				QH('p18Status', '<span>' + "Eliminando..." + '</span>');
				meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'uninstallstoreapp', value: '"' + cmd.replace(/"/g, '\\"') + '"' });
				setTimeout(function() { QH('p18Status', "Comando de eliminación enviado. Haga clic en Actualizar para actualizar"); }, 2000);
			});
		}

        function handleUninstallResponse(data) {
            try {
                var result = JSON.parse(data);
                if (result.success) {
                    QH('p18Status', '<span>✓ ' + "Exitoso" + '</span> ' + "Haga clic en Actualizar para actualizar");
                } else if (result.error) {
                    QH('p18Status', '<span>✗ ' + "ERROR: " + EscapeHtml(result.error) + '</span>');
                }
            } catch(e) {}
        }

        // ============================================
        // END INSTALLED SOFTWARE
        // ============================================

        //
        // CONSOLE
        //

        function agentConsoleHandleKeys(e) {
            if ((e.ctrlKey) || (e.altKey)) { return true; }
            var processed = 0, box = Q('p15consoleText');
            if (e.key) {
                if (e.keyCode == 13 && consoleFocus == 0) { p15consoleSend(e); processed = 1; }
                else if (e.keyCode == 8 && consoleFocus == 0) { var x = box.value; box.value = x.substring(0, x.length - 1); processed = 1; }
                else if (e.keyCode == 27) { box.value = ''; processed = 1; }
                else if ((e.keyCode == 38) || (e.keyCode == 40)) { // Arrow up || Arrow down
                    var hindex = consoleHistory.indexOf(box.value);
                    //console.log(hindex, consoleHistory);
                    if ((e.keyCode == 38) && ((consoleHistory.length - 1) > hindex)) { box.value = consoleHistory[hindex + 1]; }
                    else if ((e.keyCode == 40) && (hindex > 0)) { box.value = consoleHistory[hindex - 1]; }
                    else if ((e.keyCode == 40) && (hindex == 0)) { box.value = ''; }
                    processed = 1;
                }
                else if (e.key.length === 1) {
                    //box.value = ((box.value + e.key));
                    insertTextAtCursor(box, e.key);
                    processed = 1;
                }
            } else {
                if (e.charCode != 0 && consoleFocus == 0) { box.value = ((box.value + String.fromCharCode(e.charCode))); processed = 1; }
            }
            if (processed > 0) { return haltEvent(e); }
        }

        // Insert text at the cursor location on the
        function insertTextAtCursor(ctrl, val) {
            if (document.selection) { ctrl.focus(); sel = document.selection.createRange(); sel.text = val; }
            else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
                var start = ctrl.selectionStart, end = ctrl.selectionEnd;
                ctrl.value = ctrl.value.substring(0, start) + val + ctrl.value.substring(end, ctrl.value.length);
                ctrl.setSelectionRange(end + 1, end + 1);
            } else { ctrl.value += myValue; }
        }

        var consoleNode;
        var consoleServerText = '';
        function setupConsole() {
            if (xxcurrentView == 115) {
                // Setup server console
                var samenode = (consoleNode == 'server');
                consoleNode = 'server';

                QH('p15deviceName', ' - ' + "Mi Consola de Servidor");
                QE('p15consoleText', true);
                QH('p15statetext', '');
                QH('p15coreName', '');
                QV('p15outputselecttd', false);
                QV('p15BackButton', false); // no "back" navigation from server console
                QC('p15title').add('no-back-btn'); // add left-padding since no back button

                if (samenode == false) {
                    QH('p15agentConsoleText', consoleServerText);
                    Q('p15agentConsoleText').scrollTop = Q('p15agentConsoleText').scrollHeight;
                }
            } else {
                // Setup device console
                var samenode = (consoleNode == currentNode);
                consoleNode = currentNode;

                var mesh = meshes[consoleNode.meshid];
                var rights = GetNodeRights(currentNode);
                if ((rights & 16) != 0) {
                    if (consoleNode.consoleText == null) { consoleNode.consoleText = ''; }
                    if (samenode == false) {
                        QH('p15agentConsoleText', consoleNode.consoleText);
                        Q('p15agentConsoleText').scrollTop = Q('p15agentConsoleText').scrollHeight;
                    }
                    var online = (((consoleNode.conn & 1) != 0) || ((consoleNode.conn & 16) != 0)) ? true : false;
                    var onlineText = ((consoleNode.conn & 1) != 0) ? "El agente está en línea" : "El agente está fuera de línea"
                    if ((consoleNode.conn & 16) != 0) { onlineText += ", MQQT esta en línea" }
                    QH('p15statetext', onlineText);
                    QE('p15uploadCore', ((consoleNode.conn & 1) != 0));
                    QV('p15outputselecttd', ((consoleNode.conn & 16) != 0) || ((currentNode.pmt == 1) && ((features2 & 4) != 0)));
                    QV('p15outputselect2', ((consoleNode.conn & 16) != 0)); // MQTT channel
                    QV('p15outputselect3', ((currentNode.pmt == 1) && ((features2 & 4) != 0))); // Two-way Push Notification channel

                    var c = Q('p15outputselect').value;
                    if (((consoleNode.conn & 16) == 0) && (c == 2)) { c = 1; Q('p15outputselect').value = 1; }
                    if (((currentNode.pmt != 1) || ((features2 & 4) == 0)) && (c == 3)) { c = 1; Q('p15outputselect').value = 1; }

                    var active = false;
                    if (((consoleNode.conn & 1) != 0) && (c == 1)) { active = true; } // Agent
                    if (((consoleNode.conn & 16) != 0) && (c == 2)) { active = true; } // MQTT
                    if (((currentNode.pmt == 1) && ((features2 & 4) != 0)) && (c == 3)) { active = true; } // Push
                    QE('p15consoleText', active);
                } else {
                    QH('p15statetext', "Acceso Denegado");
                    QE('p15consoleText', false);
                    QE('p15uploadCore', false);
                    QV('p15outputselecttd', false);
                }
                QV('devListToolbarViewIcons3', ((consoleNode.conn & 1) != 0));
            }
        }

        // Clear the console for this node
        function p15consoleClear() {
            QH('p15agentConsoleText', '');
            Q('id_p15consoleClear').blur();
            if (xxcurrentView == 115) {
                consoleServerText = '';
            } else {
                consoleNode.consoleText = '';
            }
        }

        // Send a command to the agent
        var consoleHistory = [];
        function p15consoleSend(e) {
            if (e && e.keyCode != 13) return;
            var v = Q('p15consoleText').value, t = '<div style=color:green>&gt; ' + EscapeHtml(v) + '<br/></div>';

            if (xxcurrentView == 115) {
                // Send the command to the server - TODO: In the future, we may support multiple servers.
                consoleServerText += t;
                meshserver.send({ action: 'serverconsole', value: v });
            } else {
                if (((consoleNode.conn & 16) != 0) && (Q('p15outputselect').value == 2)) {
                    // Send the command to MQTT
                    t = '<div style=color:orange>' + "MQTT" + '&gt; ' + EscapeHtml(v) + '<br/></div>';
                    consoleNode.consoleText += t;
                    meshserver.send({ action: 'sendmqttmsg', topic: 'console', nodeids: [consoleNode._id], msg: v });
                } else if ((consoleNode.pmt == 1) && (Q('p15outputselect').value == 3) && ((features2 & 4) != 0)) {
                    // Send the command using push notification
                    t = '<div style=color:violet>' + "EMPUJAR" + '&gt; ' + EscapeHtml(v) + '<br/></div>';
                    consoleNode.consoleText += t;
                    meshserver.send({ action: 'pushconsole', nodeid: consoleNode._id, console: v });
                } else if ((consoleNode.conn & 1) != 0) {
                    // Send the command to the mesh agent
                    consoleNode.consoleText += t;
                    meshserver.send({ action: 'msg', type: 'console', nodeid: consoleNode._id, value: v });
                }
            }

            Q('p15agentConsoleText').innerHTML += t;
            Q('p15agentConsoleText').scrollTop = Q('p15agentConsoleText').scrollHeight;
            Q('p15consoleText').value = '';

            // Add command to history list
            if (v.length > 0) {
                // Move this command to the top if it already exists
                var j = consoleHistory.indexOf(v);
                if (j >= 0) { consoleHistory.splice(j, 1); }
                consoleHistory.unshift(v);
                consoleHistory.splice(10);
            }
        }

        // Handle Mesh Agent console data
        function p15consoleReceive(node, data, source) {
            if (node === 'serverconsole') {
                // Server console data
                data = '<div>' + EscapeHtml(data) + '</div>'
                consoleServerText += data;
                if (consoleNode == 'server') {
                    Q('p15agentConsoleText').innerHTML += data;
                    Q('p15agentConsoleText').scrollTop = Q('p15agentConsoleText').scrollHeight;
                }
            } else {
                // Agent console data
                if (source == 'MQTT') { data = '<div style=color:red>' + "MQTT" + '&gt; ' + EscapeHtml(data) + '<br/></div>'; } else { data = '<div>' + EscapeHtml(data) + '</div>' }
                if (node.consoleText == null) { node.consoleText = data; } else { node.consoleText += data; }
                if (consoleNode == node) {
                    Q('p15agentConsoleText').innerHTML += data;
                    Q('p15agentConsoleText').scrollTop = Q('p15agentConsoleText').scrollHeight;
                }
            }
        }

        // Save console text to file
        function p15downloadConsoleText() {
            saveAs(new Blob([Q('p15agentConsoleText').innerText], { type: 'application/octet-stream' }), 'console.txt');
        }

        // Called then user presses the "Change Core" button
        function p15uploadCore(e) {
            if (xxdialogMode) return;
            if (e.shiftKey == true) { meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'default' }); } // Upload default core
            else if (e.altKey == true) { meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'clear' }); } // Clear the core
            else if (e.ctrlKey == true) { p15uploadCore2(); } // Upload the core from a file
            else {
                var htmlValue = '<select id=d3coreMode class="form-select me-2">' +
                    '<option value=1>' + "Cargar el archivo de núcleo predeterminado" + '</option>' +
                    '<option value=2>' + "Borrar el núcleo" + '</option>' +
                    '<option value=3>' + "Cargar un archivo de núcleo" + '</option>' +
                    '<option value=4>' + "Agente de desconexión por Soft" + '</option>' +
                    '<option value=5>' + "Agente (Desconexión dura)" + '</option>' +
                    '<option value=6>' + "Cargar núcleo de recuperación" + '</option>' +
                    '<option value=7>' + "Sube un pequeño núcleo" + '</option>' +
                    '<option value=8>' + "Restart agent service" + '</option>' +
                    '<option value=9>' + "Forzar actualización del agente" + '</option></select>';
                setModalContent('xxAddAgent', "Realizar Acción en el Agente", addHtmlFormFloating("Acción", htmlValue));
                showModal('xxAddAgentModal', 'idx_dlgOkButton', p15uploadCoreEx);
            }
        }

        function p15uploadCoreEx() {
            if (Q('d3coreMode').value == 1) {
                // Upload default core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'default' });
            } else if (Q('d3coreMode').value == 2) {
                // Clear the core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'clear' });
            } else if (Q('d3coreMode').value == 3) {
                // Upload file as core
                p15uploadCore2();
            } else if (Q('d3coreMode').value == 4) {
                // Soft disconnect the mesh agent
                meshserver.send({ action: 'agentdisconnect', nodeid: consoleNode._id, disconnectMode: 1 });
            } else if (Q('d3coreMode').value == 5) {
                // Hard disconnect the mesh agent
                meshserver.send({ action: 'agentdisconnect', nodeid: consoleNode._id, disconnectMode: 2 });
            } else if (Q('d3coreMode').value == 6) {
                // Upload a recovery core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'recovery' });
            } else if (Q('d3coreMode').value == 7) {
                // Upload a tiny core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'tiny' });
            } else if (Q('d3coreMode').value == 8) {
                // Restart MeshAgent service
                meshserver.send({ action: 'msg', type: 'console', nodeid: consoleNode._id, value:'service restart' });
            } else if (Q('d3coreMode').value == 9) {
                // Update mesh agent
                meshserver.send({ action: 'updateAgents', nodeids: [consoleNode._id] });
            }
        }

        // Called then user opts to upload a file as core
        function p15uploadCore2() {
            if (xxdialogMode) return;
            Q('d3localmodeform').action = 'uploadmeshcorefile.ashx';
            Q('d3auth').value = authCookie;
            Q('d3filter').value = '.js';
            Q('d3attrib').value = currentNode._id;
            setModalContent('xxAddAgent', "Cargar Núcleo de Agente Mesh", '');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p15uploadCoreEx2());
            d3init();
        }

        function p15uploadCoreEx2() {
            var mode = Q('d3uploadMode').value;
            if (mode == 1) {
                // Upload local mesh agent core
                Q('d3submit').click();
            } else {
                // Upload server mesh agent code
                var files = d3getFileSel();
                if (files.length == 1) { meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'custom', path: d3filetreelocation.join('/') + '/' + files[0] }); }
            }
        }

        //
        // MY ACCOUNT
        //

        function account_viewPreviousLogins() {
            if (xxdialogMode) return;
            xxdialogTag = 'previousLogins';
            setModalContent('xxAddAgent', "Inicios de Sesión Anteriores", 'Loading...');
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            meshserver.send({ action: 'previousLogins' });
        }

        function account_manageImage(mode) {
            if (xxdialogMode) return;
            var user = (mode == 0) ? userinfo : currentUser;
            var x = '<input id=p2file type=file class="form-control" accept="image/*" onchange=account_manageImageEx()><div style=width:100%><canvas id=p2canvas width=256 height=256 style="width:256px;height:256px;margin-left:60px;margin-top:8px;border-radius:16px;background: var(--sub-menu-bg);border: 1px solid #cfcdcd; cursor: pointer;" onclick=account_canvasClick() /></div>';

            setModalContent('xxAddAgent', "Administrar Imagen de Cuenta", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_manageImageEx2(1, user._id));

            var ctx = Q('p2canvas').getContext('2d');
            if (user.accountImageRnd == null) { user.accountImageRnd = Math.floor(Math.random() * 9999999999); }
            var arg = '';
            if (mode == 1) { arg = '&id=' + user._id.split('/')[2]; }
            var myImg = new Image();
            myImg.onload = function () { ctx.clearRect(0, 0, 256, 256); ctx.drawImage(myImg, 0, 0); };
            myImg.src = ((user.flags != null) && (user.flags & 1)) ? ('userimage.ashx?rnd=' + user.accountImageRnd + arg) : 'images/user-256.png';
            QE('idx_dlgDeleteButton', (user.flags != null) && (user.flags & 1));
            QV('idx_dlgDeleteButton', (user.flags != null) && (user.flags & 1));
            Q('idx_dlgDeleteButton').onclick = () => account_manageImageEx2(2, user._id);
            QE('idx_dlgOkButton', false);
        }

        function account_canvasClick() { Q('p2file').click(); }

        function account_manageImageEx() {
            var file = Q('p2file').files[0];
            var img = new Image;
            img.onload = function () {
                var cx = 0, cy = 0, min = Math.min(img.width, img.height);
                if (img.width > min) { cx = (img.width - min) / 2; }
                if (img.height > min) { cy = (img.height - min) / 2; }
                var ctx = Q('p2canvas').getContext('2d');
                ctx.imageSmoothingEnabled = true;
                ctx.webkitImageSmoothingEnabled = true;
                ctx.mozImageSmoothingEnabled = true;
                ctx.clearRect(0, 0, 256, 256);
                ctx.drawImage(img, cx, cy, min, min, 0, 0, 256, 256);
                QE('idx_dlgOkButton', true);
            }
            img.src = URL.createObjectURL(file);
        }

        function account_manageImageEx2(b, userid) {
            // Send updated image, or 0 if we pressed the delete button
            meshserver.send({ action: 'updateUserImage', userid: userid, image: (b == 2) ? 0 : Q('p2canvas').toDataURL(Q('p2file').files[0].type) });
            if (b == 2) { xxModal.hide(); } // Close dialog on delete
            //meshserver.send({ action: 'updateUserImage', image: (b == 2)?0:Q('p2canvas').toDataURL('image/png', 0.8) });
        }

        function account_managePhone() {
            if (xxdialogMode || ((features & 0x02000000) == 0)) return;
            var x;
            if (userinfo.phone != null) {
                x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
                x += '<td style=text-align:center><div style=padding:6px>' + "Número de teléfono verificado" + '</div><div style=font-size:20px>' + EscapeHtml(userinfo.phone) + '</div>';
                x += '<div style=margin:10px><label><input id=d2delPhone type=checkbox class="form-check-input me-2" onclick=account_managePhoneRemoveValidate() />' + "Remover número de teléfono" + '</label></div>';
                setModalContent('xxAddAgent', "Notificaciones Telefónicas", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_managePhoneRemove());
                account_managePhoneRemoveValidate();
            } else {
                x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
                x += '<td>' + "Ingresa tu número de teléfono con capacidad para SMS. Una vez verificado, el número puede usarse para la verificación de inicio de sesión y otras notificaciones.";
                x += '<br /><br /><div style=width:100%;text-align:center>' + "Número de teléfono:" + ' <input type=tel pattern="[0-9]" autocomplete="tel" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=account_managePhoneValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneValidate(1)"></div></table>';
                xxdialogTag = 'verifyPhone';
                setModalContent('xxAddAgent', "Notificaciones Telefónicas", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_managePhoneAdd());
                Q('d2phoneinput').focus();
                account_managePhoneValidate();
            }
        }

        function isPhoneNumber(x) { var ok = true; if (x.startsWith('+')) { x = x.substring(1); } for (var i = 0; i < x.length; i++) { var c = x.charCodeAt(i); if (((c < 48) || (c > 57)) && (c != 32) && (c != 45) && (c != 46)) { ok = false; } } return ok && (x.length >= 10); }
        function account_managePhoneValidate(x) { var ok = isPhoneNumber(Q('d2phoneinput').value); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
        function account_managePhoneCodeValidate(x) { var ok = (Q('d2phoneCodeInput').value.length == 6) && Q('d2phoneCodeInput').value.match(/[0-9]/); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
        function account_managePhoneConfirm(b, tag) { meshserver.send({ action: 'confirmPhone', code: Q('d2phoneCodeInput').value, cookie: tag }); }
        function account_managePhoneAdd() { if (isPhoneNumber(Q('d2phoneinput').value) == false) return; QE('d2phoneinput', false); meshserver.send({ action: 'verifyPhone', phone: Q('d2phoneinput').value }); }
        function account_managePhoneRemove() { if (Q('d2delPhone').checked) { meshserver.send({ action: 'removePhone' }); } }
        function account_managePhoneRemoveValidate() { QE('idx_dlgOkButton', Q('d2delPhone').checked); }

        function account_manageMessaging() {
            if (xxdialogMode || ((features2 & 0x02000000) == 0)) return;
            var x;
            if (userinfo.msghandle != null) {
                x = '<table style=width:100%><tr><td style=width:56px><img src="images/messaging40.png" style=padding:8px>';
                x += '<td style=text-align:center><div style=padding:6px>' + "Usuario Verificado" + '</div><div style=font-weight:bold>' + EscapeHtml(userinfo.msghandle) + '</div>';
                x += '<div style=margin:10px><label><input id=d2delPhone type=checkbox class="form-check-input me-2" onclick=account_managePhoneRemoveValidate() />' + "Remover mensajes" + '</label></div>';
                setModalContent('xxAddAgent', "Notificaciones de Mensajería", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_manageMessagingRemove());
                account_managePhoneRemoveValidate();
            } else {
                x = '<table style=width:100%><tr><td style=width:56px;vertical-align:top><img src="images/messaging40.png" style=padding:8px>';
                x += '<td>' + "Ingresa a tu servicio de mensajería y Usuario. Una vez verificado, este servidor puede enviarle verificación de inicio de sesión y otras notificaciones." + '<br /><br />';
                var y = '<select id=d2serviceselect style=width:160px;margin-left:8px onchange=account_manageMessagingValidate()>';
                if ((serverinfo.userMsgProviders & 1) != 0) { y += '<option value=1>' + "Telegram" + '</option>'; }
                if ((serverinfo.userMsgProviders & 4) != 0) { y += '<option value=4>' + "Discord" + '</option>'; }
                if ((serverinfo.userMsgProviders & 8) != 0) { y += '<option value=8>' + "XMPP" + '</option>'; }
                if ((serverinfo.userMsgProviders & 16) != 0) { y += '<option value=16>' + "CallMeBot" + '</option>'; }
                if ((serverinfo.userMsgProviders & 32) != 0) { y += '<option value=32>' + "Pushover" + '</option>'; }
                if ((serverinfo.userMsgProviders & 64) != 0) { y += '<option value=64>' + "ntfy" + '</option>'; }
                if ((serverinfo.userMsgProviders & 128) != 0) { y += '<option value=128>' + "Zulip" + '</option>'; }
                if ((serverinfo.userMsgProviders & 256) != 0) { y += '<option value=256>' + "Slack" + '</option>'; }
                y += '</select>';
                x += '<table><tr><td>' + "Servicio" + '<td>' + y;
                x += '<tr><td>' + "Usuario" + '<td><input maxlength=1024 style=width:160px;margin-left:8px id=d2handleinput onKeyUp=account_manageMessagingValidate() onkeypress="if (event.key==\'Enter\') account_manageMessagingValidate(1)">';
                x += '</table>';
                if (serverinfo.discordUrl) { x += '<div id=d2discordurl style=display:none><br /><a href=' + serverinfo.discordUrl + ' target="_discord">' + "Unete a este canal de Discord para recibir notificaciones." + '</a></div>'; }
                x += '<div id=d2callmebotinfo style=display:none><br /><a href=https://www.callmebot.com/blog/free-api-signal-send-messages/ target="_callmebot">' + "Signal" + '</a>, <a href=https://www.callmebot.com/blog/free-api-whatsapp-messages/ target="_callmebot">' + "Whatsapp" + '</a>, <a href=https://www.callmebot.com/blog/free-api-facebook-messenger/ target="_callmebot">' + "Facebook" + '</a>, <a href=https://www.callmebot.com/blog/telegram-text-messages/ target="_callmebot">' + "Telegram" + '</a></div>';
                x += '<div id=d2pushoverinfo style=display:none><br /><a href=https://pushover.net/ target="_pushover">' + "Informacion en Pushover.net" + '</a></div>';
                x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Servicio gratuito en ntfy.sh" + '</a></div>';
                x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Slack Webhook Setup" + '</a></div>';
                xxdialogTag = 'VerifyMessaging';
                setModalContent('xxAddAgent', "Notificaciones de Mensajería", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_manageMessagingAdd());
                Q('d2handleinput').focus();
                account_manageMessagingValidate();
            }
        }

        function account_manageMessagingValidate(x) {
            if (serverinfo.discordUrl) { QV('d2discordurl', Q('d2serviceselect').value == 4); }
            QV('d2callmebotinfo', Q('d2serviceselect').value == 16);
            QV('d2pushoverinfo', Q('d2serviceselect').value == 32);
            QV('d2ntfyinfo', Q('d2serviceselect').value == 64);
            QV('d2slackinfo', Q('d2serviceselect').value == 256);
            if (Q('d2serviceselect').value == 4) { Q('d2handleinput')['placeholder'] = "Usuario:0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "nombredeusuario@servidor.com"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Clave de usuario"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Tema"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "nombredeusuario@ejemplo.com"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Nombre de Usuario"; }
            var ok = (Q('d2handleinput').value.length > 0); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); }
        }
        function account_manageMessagingAdd() { if (Q('d2handleinput').value.length == 0) return; QE('d2handleinput', false); meshserver.send({ action: 'verifyMessaging', service: Q('d2serviceselect').value, handle: Q('d2handleinput').value }); return false; }
        function account_manageMessagingConfirm(b, tag) { meshserver.send({ action: 'confirmMessaging', code: Q('d2phoneCodeInput').value, cookie: tag }); }
        function account_manageMessagingRemove() { if (Q('d2delPhone').checked) { meshserver.send({ action: 'removeMessaging' }); } }

        function account_manageAuthEmail() {
            if (xxdialogMode || ((features & 0x00800000) == 0)) return;
            var emailU2Fenabled = ((userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
            setModalContent('xxAddAgent', "Autenticación de Correo Electrónico", "Cuando esté habilitado, en cada inicio de sesión, se le dará la opción de recibir un token de inicio de sesión en su cuenta de correo electrónico para mayor seguridad." + '<br /><br /><label><input id=email2facheck type=checkbox class="form-check-input me-2" ' + (emailU2Fenabled ? 'checked' : '') + '/>' + "Habilitar autenticación de dos factores por correo electrónico." + '</label>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                if (emailU2Fenabled != Q('email2facheck').checked) { meshserver.send({ action: 'otpemail', enabled: Q('email2facheck').checked }); }
            });
        }

        function account_manageAuthDuo() {
            if (xxdialogMode || ((features2 & 0x20000000) == 0)) return;
            var duoU2Fenabled = ((userinfo.otpduo == 1));
            if (duoU2Fenabled == false) {
                setModalContent('xxAddAgent', "Duo Authentication", "Confirm enabling of Duo 2FA login security. Once enabled you will be given the option to use Duo at login for added security. Click ok to go thru the steps to enable Duo." + '<p style="text-align:center;margin-top:10px"><img src="images/duo-2fa-250.png"></p>');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    window.location.href = '/add-duo?rurl=' + encodeURIComponentEx(window.location.href) + ((urlargs.key)?('&key=' + urlargs.key):'');
                });
            } else {
                setModalContent('xxAddAgent', "Duo Authentication", '<p><label><input id=duo2facheck type=checkbox onclick=account_manageAuthDuoConfirm() />' + ' ' + "Confirm disabling 2FA Duo login security." + '</label></p>' + '<p style="text-align: center"><img src="images/duo-2fa-250-disable.png"></p>');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    meshserver.send({ action: 'otpduo', enabled: false });
                });
                QE('idx_dlgOkButton', false);
            }
        }

        function account_manageAuthDuoConfirm() {
            QE('idx_dlgOkButton', Q('duo2facheck').checked);
        }

        function account_manageAuthApp() {
            if (xxdialogMode || ((features & 4096) == 0)) return;
            if (userinfo.otpsecret == 1) { account_removeOtp(); } else { account_addOtp(); }
            return false;
        }

        function account_addOtp() {
            if (xxdialogMode || (userinfo.otpsecret == 1) || ((features & 4096) == 0)) return;
            xxdialogTag = 'otpauth-request';
            setModalContent('xxAddAgent', "Aplicación de Autenticación", ('<div id=d2optinfo>' + "Cargando..." + '</div>'));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'otpauth-setup', secret: Q('d2optsecret').attributes.secret.value, token: Q('d2otpauthinput').value }); return false; });
            meshserver.send({ action: 'otpauth-request' });
        }

        function account_addOtpCheck(e) {
            var tokenIsValid = (Q('d2otpauthinput').value.length == 6);
            QE('idx_dlgOkButton', tokenIsValid);
            if (e && (e.keyCode == 13) && tokenIsValid) { dialogclose(1); }
        }

        function account_removeOtp() {
            if (xxdialogMode || (userinfo.otpsecret != 1) || ((features & 4096) == 0)) return;
            setModalContent('xxAddAgent', "Aplicación de Autenticación", "¿Confirmar la eliminación del inicio de sesión usando autenticación de 2 pasos?");
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'otpauth-clear' }); });
        }

        function account_manageOtp(action) {
            if ((features & 4096) == 0) return false;
            if (count2factoraAuths() > 0) { meshserver.send({ action: 'otpauth-getpasswords', subaction: action }); }
            return false;
        }

        function account_managePushAuthDev() {
            if (xxdialogMode || ((features2 & 0x40) == 0)) return;
            if (userinfo.otpdev == 1) {
                // Remove the 2FA device
                setModalContent('xxAddAgent', "Dispositivo de Autenticación", "¿Confirmar la eliminación del dispositivo de autenticación push?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'otpdev-clear' }); });
            } else {
                // Create a list of all mobile devices
                var mobileDevices = [];
                for (var i in nodes) { var node = nodes[i]; if ((node.agent != null) && (node.agent.id == 14) && (node.pmt == 1) && (GetNodeRights(node) == 0xFFFFFFFF)) { mobileDevices.push(node); } }
                if (mobileDevices.length == 0) {
                    // No mobile devices found
                    setModalContent('xxAddAgent', "Dispositivo de Autenticación", 'In order to use push notification authentication, a mobile device must be setup in your account with full rights.');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else {
                    // Set a 2FA device
                    var x = "Selecciona un dispositivo para registrarse para la autenticación de notificaciones push. Una vez seleccionado, el dispositivo solicitará confirmación." + '<br /><br />';
                    var y = '<select id=d2devselect class="form-select">';
                    for (var i in mobileDevices) { y += '<option value="' + mobileDevices[i]._id + '">' + EscapeHtml(mobileDevices[i].name) + '</option>'; }
                    y += '</select>';
                    x += addHtmlFormFloating("Dispositivo", y);
                    setModalContent('xxAddAgent', "Dispositivo de Autenticación", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'otpdev-set', nodeid: Q('d2devselect').value }); });
                }
            }
            return false;
        }

        function account_manageHardwareOtp() {
            if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-hardware-manage')) { dialogclose(0); }
            if (xxdialogMode || ((features & 4096) == 0)) return false;
            meshserver.send({ action: 'otp-hkey-get' });
            return false;
        }

        function account_addhkey(type) {
            if (type == 3) {
                var x = "Escribe el nombre de la clave para agregar." + '<br /><br />';
                x += addHtmlFormFloating("Nombre de la Llave", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "Mi Llave" + '" onkeyup=account_addhkeyValidate(event,2) />');
            } else if (type == 2) {
                var x = "Escribe la clave, selecciona el cuadro OTP y presiona el botón en YubiKey&trade;." + '<br /><br />';
                x += addHtmlFormFloating("Nombre de la Llave", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "Mi Llave" + '" onkeyup=account_addhkeyValidate(event,1) />');
                x += addHtmlFormFloating("YubiKey&trade; OTP", '<input id=dp1key class="form-control" autocomplete=off onkeyup=account_addhkeyValidate(event,2) />');
            }
            setModalContent('xxAddAgent', "Agregar Llave de Seguridad", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_addhkeyEx(3, type));
            Q('dp1keyname').focus();
        }

        function account_addhkeyValidate(e, action) {
            if ((e != null) && (e.keyCode == 13)) { if (action == 2) { dialogclose(1); } else { Q('dp1key').focus(); } }
        }

        function account_addhkeyEx(button, type) {
            var name = Q('dp1keyname').value;
            if (name == '') { name = 'MyKey'; }
            if (type == 2) {
                meshserver.send({ action: 'otp-hkey-yubikey-add', name: name, otp: Q('dp1key').value });
                xxdialogTag = 'otpauth-hardware-manage';
                setModalContent('xxAddAgent', "Agregar Llave de Seguridad", '<br />' + "Comprobando..." + '<br /><br /><br />');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            } else if (type == 3) {
                meshserver.send({ action: 'webauthn-startregister', name: name });
                return false;
            }
        }

        function account_removehkey(index) {
            meshserver.send({ action: 'otp-hkey-remove', index: index });
            meshserver.send({ action: 'otp-hkey-get' });
        }

        var loclist = { 'af': "Afrikaans", 'sq': "Albanés", 'ar': "Árabe (estándar)", 'ar-dz': "Árabe (Algeria)", 'ar-bh': "Árabe (Bahrain)", 'ar-eg': "Arabe (Egipto)", 'ar-iq': "Arabe (Irak)", 'ar-jo': "Árabe (Jordania)", 'ar-kw': "Árabe (Kuwait)", 'ar-lb': "Árabe (Líbano)", 'ar-ly': "Árabe (Libia)", 'ar-ma': "Árabe (Marruecos)", 'ar-om': "Árabe (Omán)", 'ar-qa': "Árabe (Qatar)", 'ar-sa': "Árabe (Arabia Saudita)", 'ar-sy': "Árabe (Siria)", 'ar-tn': "Árabe (Túnez)", 'ar-ae': "Árabe (U.A.E.)", 'ar-ye': "Árabe (Yemen)", 'an': "Aragonés", 'hy': "Armenio", 'as': "Asamés", 'ast': "Asturiano", 'az': "Azerbaiyano", 'eu': "Vasco", 'bg': "Búlgaro", 'be': "Bielorruso", 'bn': "Bengalí", 'bs': "Bosnio", 'br': "Bretón", 'my': "Birmano", 'ca': "Catalan", 'ch': "Chamorro", 'ce': "Checheno", 'zh': "Chino", 'zh-hk': "Chino (Hong Kong)", 'zh-cn': "Chino (PRC)", 'zh-sg': "Chino (Singapur)", 'zh-tw': "Chino (Taiwán)", 'cv': "Chuvash", 'co': "Corso", 'cr': "Cree", 'hr': "Croata", 'cs': "Checo", 'da': "Danés", 'nl': "Holandés (estándar)", 'nl-be': "Holandés (belga)", 'en': "Inglés", 'en-au': "Inglés (Australia)", 'en-bz': "Inglés (belice)", 'en-ca': "Inglés (Canadá)", 'en-ie': "Inglés (Irlanda)", 'en-jm': "Inglés (Jamaica)", 'en-nz': "Inglés (Nueva Zelanda)", 'en-ph': "Inglés (Filipinas)", 'en-za': "Inglés (Sudáfrica)", 'en-tt': "Inglés (Trinidad y Tobago)", 'en-gb': "Inglés (Reino Unido)", 'en-us': "Inglés (Estados Unidos)", 'en-zw': "Inglés (Zimbabwe)", 'eo': "Esperanto", 'et': "Estonio", 'fo': "Faeroese", 'fa': "Farsi (Persa)", 'fj': "Fiyiano", 'fi': "Finlandés", 'fr': "Francés (estándar)", 'fr-be': "Francés (Bélgica)", 'fr-ca': "Francés (Canadá)", 'fr-fr': "Francés (Francia)", 'fr-lu': "Francés (Luxemburgo)", 'fr-mc': "Francés (Mónaco)", 'fr-ch': "Francés (Suiza)", 'fy': "Friso", 'fur': "Friuliano", 'gd': "Gaélico (Escocés)", 'gd-ie': "Gaélico (Irlandés)", 'gl': "Gallego", 'ka': "Georgiano", 'de': "Alemán (Estándar)", 'de-at': "Alemán (Austria)", 'de-de': "Alemán (Alemania)", 'de-li': "Alemán (Liechtenstein)", 'de-lu': "Alemán (Luxemburgo)", 'de-ch': "Alemán (Suiza)", 'el': "Griego", 'gu': "Gujarati", 'ht': "Haitiano", 'he': "Hebreo", 'hi': "Hindi", 'hu': "Hungaro", 'is': "Islandés", 'id': "Indonesio", 'iu': "Inuktitut", 'ga': "Irlandesa", 'it': "Italiano (estándar)", 'it-ch': "Italiano (Suiza)", 'ja': "Japones", 'kn': "Kannada", 'ks': "Cachemira", 'kk': "Kazajo", 'km': "Jemer", 'ky': "Kirghiz", 'tlh': "Klingon", 'ko': "Coreano", 'ko-kp': "Coreano (Corea del Norte)", 'ko-kr': "Coreano (Corea del Sur)", 'la': "Latin", 'lv': "Letona", 'lt': "Lituano", 'lb': "Luxemburgués", 'mk': "Macedonio Macedonia", 'ms': "Malayo", 'ml': "Malayalam", 'mt': "Maltés", 'mi': "Maori", 'mr': "Marathi", 'mo': "Moldavo", 'nv': "Navajo", 'ng': "Ndonga", 'ne': "Nepali", 'no': "Noruego", 'nb': "Noruego (Bokmal)", 'nn': "Noruego (Nynorsk)", 'oc': "Occitano", 'or': "Oriya", 'om': "Oromo", 'fa-ir': "Persa / Irán", 'pl': "Polaco", 'pt': "Portugues", 'pt-br': "Portugues (Brazil)", 'pa': "Punjabi", 'pa-in': "Punjabi (India)", 'pa-pk': "Punjabi (Pakistan)", 'qu': "Quechua", 'rm': "Retorrománico", 'ro': "Rumano", 'ro-mo': "Rumano (Moldavia)", 'ru': "Ruso", 'ru-mo': "Ruso (Moldavia)", 'sz': "Sami (Lapona)", 'sg': "Sango", 'sa': "Sánscrito", 'sc': "Sardo", 'sd': "Sindhi", 'si': "Cingalés", 'sr': "Serbio", 'sk': "Eslovaco", 'sl': "Esloveno", 'so': "Somani", 'sb': "Sorbio", 'es': "Español", 'es-ar': "Español (Argentina)", 'es-bo': "Español (Bolivia)", 'es-cl': "Español (Chile)", 'es-co': "Español (Colombia)", 'es-cr': "Español (Costa Rica)", 'es-do': "Español (República Dominicana)", 'es-ec': "Español (Ecuador)", 'es-sv': "Español (El Salvador)", 'es-gt': "Español (Guatemala)", 'es-hn': "Español (Honduras)", 'es-mx': "Español (Mexico)", 'es-ni': "Español (Nicaragua)", 'es-pa': "Español (Panama)", 'es-py': "Español (Paraguay)", 'es-pe': "Español (Peru)", 'es-pr': "Español (Puerto Rico)", 'es-es': "Español (España)", 'es-uy': "Español (Uruguay)", 'es-ve': "Español (Venezuela)", 'sx': "Sutu", 'sw': "Swahili", 'sv': "Sueco", 'sv-fi': "Sueco (Finlandia)", 'sv-sv': "Sueco (Suecia)", 'ta': "Tamil", 'tt': "Tatar", 'te': "Teluga", 'th': "Tailandés", 'tig': "Tigre", 'ts': "Tsonga", 'tn': "Tswana", 'tr': "Turco", 'tk': "Turcomanos", 'uk': "Ucraniano", 'hsb': "Sorbio superior", 'ur': "Urdu", 've': "Venda", 'vi': "Vietnamita", 'vo': "Volapük", 'wa': "Valón", 'cy': "Galés", 'xh': "Xhosa", 'ji': "Yiddish", 'zu': "Zulu" };
        var loclistex = { 'zh-chs': "Chino (simplificado)", 'zh-cht': "Chino tradicional)" };
        function account_showLocalizationSettings() {
            if (xxdialogMode) return false;
            var n = getstore('loctag', 0), y = '';
            var x = '<select id=d2locselect class="form-select"><option value="*">' + "Valor del navegador del usuario" + '</option>';
            for (var i in loclist) { x += '<option value="' + i + '"' + ((n == i) ? ' selected' : '') + '>' + i + ' - ' + loclist[i] + '</option>'; }
            x += '</select>';
            if (serverinfo.languages && serverinfo.languages.length > 0) {
                y += "Cambiar el idioma requerirá una actualización de la página." + '<br /><br />';
                var z = '<select id=d2langselect class="form-select"><option value="*">' + "Valor del navegador del usuario" + '</option>';
                for (var i in serverinfo.languages) {
                    var lang = serverinfo.languages[i];
                    z += '<option value="' + lang + '"' + ((userinfo.lang == lang) ? ' selected' : '') + '>' + lang + ' - ' + (loclist[lang] ? loclist[lang] : loclistex[lang]) + '</option>';
                }
                z += '</select>';
                y += addHtmlFormFloating("Idioma", z);
            }
            y += addHtmlFormFloating("Fecha & Hora", x);

            if ((userinfo.siteadmin == 0xFFFFFFFF) && (domain == '')) {
                y += '<br /><a rel="noreferrer noopener" target="_blank" href="translator.htm">' + "Ayuda a traducir MeshCentral" + '</a>';
            }

            setModalContent('xxAddAgent', "Opciones de Hora e Idioma", y);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', account_showLocalizationSettingsEx);
            return false;
        }

        function account_showLocalizationSettingsEx() {
            // Set user language
            var langSelectElement = Q('d2langselect');
            var lang = langSelectElement ? langSelectElement.value : null;
            if ((lang == '*') && (userinfo.lang == null)) { lang = userinfo.lang; }
            if (lang != userinfo.lang) { meshserver.send({ action: 'changelang', lang: lang }); }

            // Set date localization
            var n = getstore('loctag', 0);
            var m = Q('d2locselect').value;
            if (n != m) {
                if (m != '*') { args.locale = m; } else { delete args.locale; }
                putstore('loctag', args.locale);
                mainUpdate(0xFFFFFFFF); // Refresh everything.
            }
        }

        function account_enableNotifications() {
            if (Notification) { Notification.requestPermission().then(function (permission) { QV('accountEnableNotificationsSpan', permission != 'granted'); setupServiceWorker(); }); }
            return false;
        }

        function account_createLoginToken() {
            if (xxdialogMode) return false;
            var y = '', x = "Crea un nombre de usuario y una contraseña temporales que se puedan utilizar como inicio de sesión alternativo a tu cuenta. Esto es útil para permitir que otras herramientas u otros servicios accedan a tu cuenta." + '<br /><br />';
            var options = { 0: "Ilimitado", 1: "1 minuto", 5: "5 minutos", 10: "10 minutos", 15: "15 minutos", 30: "30 minutos", 45: "45 minutos", 60: "60 minutos", 120: "2 horas", 240: "4 horas", 480: "8 horas", 720: "12 horas", 960: "16 horas", 1440: "24 horas", 2880: "2 días", 5760: "4 dias", 10080: "7 días" }
            for (var i in options) { y += '<option value=' + i + '>' + options[i] + '</option>'; }
            x += '<div class="form-floating mb-3">';
            x += '<input type=text class="boxsize form-control" id=d2tokenName maxlength="100" placeholder="' + "Nombre del token" + '" onchange=account_createLoginTokenValidate() onkeyup=account_createLoginTokenValidate()>';
            x += '<label for=d2tokenName>' + "Nombre del token" + '</label>';
            x += '</div>';
            x += addHtmlFormFloating("Tiempo de Expiración", '<select class="boxsize form-select" id=d2tokenExpire>' + y + '</select>')

            setModalContent('xxAddAgent', "Crear Token de Inicio de Sesión", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', account_createLoginTokenEx);

            QE('idx_dlgOkButton', false);
            Q('d2tokenName').focus();
        }

        function account_createLoginTokenValidate() {
            QE('idx_dlgOkButton', Q('d2tokenName').value.length > 0);
        }

        function account_createLoginTokenEx() {
            meshserver.send({ action: 'createLoginToken', name: Q('d2tokenName').value, expire: parseInt(Q('d2tokenExpire').value) });
            return false;
        }

        function account_showAccountNotifySettings() {
            if (xxdialogMode) return false;
            var x = '';
            x += '<div class="form-check"><label><input id=p2notifyPlayNotifySound class="form-check-input me-2" type=checkbox />' + "Sonido de notificación" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyGroupName class="form-check-input me-2" type=checkbox />' + "Mostrar el nombre del grupo de dispositivos." + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Conexiones de dispositivo" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconexiones de dispositivo" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Eventos de escritorio y serial de Intel&reg; AMT" + '</label></div>';

            setModalContent('xxAddAgent', "Opciones de Notificación", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', account_showAccountNotifySettingsEx);

            var n = getstore('notifications', 0);
            Q('p2notifyPlayNotifySound').checked = (n & 1);
            Q('p2notifyIntelDeviceConnect').checked = (n & 2);
            Q('p2notifyIntelDeviceDisconnect').checked = (n & 4);
            Q('p2notifyIntelAmtKvmActions').checked = (n & 8);
            Q('p2notifyGroupName').checked = (n & 16);
            return false;
        }

        function account_showAccountNotifySettingsEx() {
            var n = 0;
            n += Q('p2notifyPlayNotifySound').checked ? 1 : 0;
            n += Q('p2notifyIntelDeviceConnect').checked ? 2 : 0;
            n += Q('p2notifyIntelDeviceDisconnect').checked ? 4 : 0;
            n += Q('p2notifyIntelAmtKvmActions').checked ? 8 : 0;
            n += Q('p2notifyGroupName').checked ? 16 : 0;
            putstore('notifications', n);
        }

        function account_showVerifyEmail() {
            if (xxdialogMode || (userinfo.emailVerified == true) || (serverinfo.emailcheck != true)) return false;
            var x = "Haz clic en Aceptar para enviar un correo de verificación a:" + '<br /><div style=padding:8px><b>' + EscapeHtml(userinfo.email) + '</b></div>' + "Espera unos minutos para recibir la verificación.";
            setModalContent('xxAddAgent', "Verificación de Correo Electrónico", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => account_showVerifyEmailEx());
            return false;
        }

        function account_showVerifyEmailEx() {
            meshserver.send({ action: 'verifyemail', email: userinfo.email });
        }

        function account_showChangeEmail() {
            if (xxdialogMode) return false;
            var x = "Cambia la dirección de correo electrónico de su cuenta aquí." + '<br /><br />';
            x += '<div class="form-floating"><input id=dp2email class="form-control" maxlength=256 onchange=account_validateEmail() onkeyup=account_validateEmail(event) placeholder=Email /> <label for=dp2email>Email</label></div>';

            setModalContent('xxAddAgent', "Cambio de Dirección de Correo Electrónico", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', account_changeEmail);

            if (userinfo.email != null) { Q('dp2email').value = userinfo.email; }
            account_validateEmail();
            Q('dp2email').focus();
            return false;
        }

        function account_validateEmail(e, email) {
            QE('idx_dlgOkButton', validateEmail(Q('dp2email').value) && (Q('dp2email').value != userinfo.email));
            if ((e != null) && (e.keyCode == 13)) { dialogclose(1); }
        }

        function account_changeEmail() {
            meshserver.send({ action: 'changeemail', email: Q('dp2email').value });
        }

        function account_showDeleteAccount() {
            if (xxdialogMode) return false;
            var x = "Para eliminar esta cuenta, escribe la contraseña de la cuenta en los dos cuadros a continuación y presiona Aceptar." + '<br /><br />';
            x += '<form method=post>';
            x += '<input type=hidden name=action value=deleteaccount />';
            x += '<input type=hidden name=authcookie value="' + authCookie + '" />';
            x += '<div class="container">';
            x += '<div class="form-floating mb-2">';
            x += '<input type=password class="form-control" id=apassword1 name=apassword1 autocomplete=off placeholder="Password" onchange=account_validateDeleteAccount() onkeyup=account_validateDeleteAccount()>';
            x += '<label for="apassword1">Password</label>';
            x += '</div>';
            x += '<div class="form-floating mb-3">';
            x += '<input type=password class="form-control" id=apassword2 name=apassword2 autocomplete=off placeholder="Confirm Password" onchange=account_validateDeleteAccount() onkeyup=account_validateDeleteAccount()>';
            x += '<label for=apassword2>Confirm Password</label>';
            x += '</div>';
            x += '</div>';
            x += '</form>';

            setModalContent('xxAddAgent', "Eliminar Cuenta", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                dialogclose(1);
            });

            account_validateDeleteAccount();
            Q('apassword1').focus();
            return false;
        }

        function account_showChangePassword() {
            if (xxdialogMode) return false;
            var x = "Cambia la contraseña de tu cuenta ingresando la contraseña anterior y la contraseña nueva dos veces en los siguientes cuadros.";
            if (features & 0x00010000) { " Se puede usar una pista de contraseña, pero no se recomienda."; }
            x += '<br /><br />';
            //x += "<form action='" + domainUrl + "changepassword' method=post>";
            x += '<div class="container">';
            x += '<div class="form-floating mb-2">';
            x += '<input type=password class="form-control" id=apassword0 name=apassword0 autocomplete=off placeholder="Old password" onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword()>';
            x += '<label for=apassword0>Old password</label>';
            x += '</div>';
            x += '<div class="form-floating mb-2">';
            x += '<input type=password class="form-control" id=apassword1 name=apassword1 autocomplete=off placeholder="New password" onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword()>';
            x += '<label for=apassword1>New password</label>';
            x += '<b><span id=dxPassWarn></span></b>';
            x += '</div>';
            x += '<div class="form-floating mb-2">';
            x += '<input type=password class="form-control" id=apassword2 name=apassword2 autocomplete=off placeholder="Confirm new password" onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword()>';
            x += '<label for=apassword2>Confirm new password</label>';
            x += '</div>';
            if (features & 0x00010000) {
                x += '<div class="form-floating mb-2">';
                x += '<input type=text class="form-control" id=apasswordhint name=apasswordhint maxlength=250 autocomplete=off onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword()>';
                x += '<label for=apasswordhint>Password hint</label>';
                x += '</div>';
            }
            x += '</div>';

            if (passRequirements) {
                var r = [], rc = 0;
                for (var i in passRequirements) { if ((i != 'reset') && (i != 'hint')) { r.push(i + ':' + passRequirements[i]); rc++; } }
                if (rc > 0) { x += '<br /><span style=font-size:x-small>' + "Requerimientos: " + r.join(', ') + '.</span>'; }
            }
            x += '<br />';
            //x += '<br /><div style=padding:10px;margin-bottom:4px>';
            //x += '<input id=account_dlgCancelButton type=button value=Cancel style=float:right;width:80px;margin-left:5px onclick=dialogclose(0)>';
            //x += '<input id=account_dlgOkButton type=submit value=OK style="float:right;width:80px" onclick=dialogclose(1)>';
            //x += '</div><br /></form>';

            setModalContent('xxAddAgent', "Cambiar Contraseña", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', account_showChangePasswordEx);

            Q('apassword0').focus();
            account_validateNewPassword();
            return false;
        }

        function account_showChangePasswordEx() {
            if (Q('apassword1').value == Q('apassword2').value) {
                var r = { action: 'changepassword', oldpass: Q('apassword0').value, newpass: Q('apassword1').value };
                if (features & 0x00010000) { r.hint = Q('apasswordhint').value; }
                meshserver.send(r);
            }
        }

        function account_showThemesSwitcher() {
            if (xxdialogMode) return false;
            var themes = [
                { value: 'default', label: "Por Defecto" },
                { value: 'cerulean', label: "Cerulean" },
                { value: 'cosmo', label: "Cosmo" },
                { value: 'cyborg', label: "Cyborg" },
                { value: 'darkly', label: "Darkly" },
                { value: 'flatly', label: "Flatly" },
                { value: 'journal', label: "Journal" },
                { value: 'litera', label: "Litera" },
                { value: 'lumen', label: "Lumen" },
                { value: 'lux', label: "Lux" },
                { value: 'materia', label: "Materia" },
                { value: 'minty', label: "Minty" },
                { value: 'morph', label: "Morph" },
                { value: 'pulse', label: "Pulse" },
                { value: 'sandstone', label: "Sandstone" },
                { value: 'simplex', label: "Simplex" },
                { value: 'sketchy', label: "Sketchy" },
                { value: 'solar', label: "Solar" },
                { value: 'spacelab', label: "Spacelab" },
                { value: 'united', label: "United" },
                { value: 'vapor', label: "Vapor" },
                { value: 'yeti', label: "Yeti" },
                { value: 'zephyr', label: "Zephyr" }
            ];

            var currentTheme = getstore('theme') || 'default';
            var lastThemes = JSON.parse(getstore('lastThemes') || '[]');

            // Create a set of themes already in "Recent Themes" to avoid duplicates
            var recentThemesSet = new Set(lastThemes);
            var x = '<div class="form-group"><label for="theme-switcher">' + "Seleccione un tema" + ':</label>';
            x += '<select id="theme-switcher" class="form-select" onchange="account_switchThemeEx()">';
            // Add "Recent Themes" optgroup
            if (lastThemes.length > 0) {
                x += '<optgroup label="' + "Temas recientes" + '"">';
                lastThemes.forEach(function (theme) {
                    var selected = theme === currentTheme ? ' selected' : '';
                    x += `<option value="${theme}"${selected}>${theme.charAt(0).toUpperCase() + theme.slice(1)}</option>`;
                });
                x += '</optgroup>';
            }
            // Add "All Themes" optgroup, excluding duplicates
            x += '<optgroup label="' + "Todos los temas" + '">';
            themes.forEach(function (theme) {
                if (!recentThemesSet.has(theme.value)) {
                    var selected = theme.value === currentTheme ? ' selected' : '';
                    x += `<option value="${theme.value}"${selected}>${theme.label}</option>`;
                }
            });
            x += '</optgroup>';
            x += '</select></div>';

            setModalContent('xxAddAgent', "Cambiar tema", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            return false;
        }

        function account_switchThemeEx() {
            var themeSwitcher = document.getElementById('theme-switcher');
            const selectedTheme = themeSwitcher.value;
            const safeTheme = ((selectedTheme != 'default') ? encodeURIComponent(selectedTheme) : encodeURIComponent('..'));
            var themeStylesheet = document.getElementById('theme-stylesheet');
            themeStylesheet.href = `styles/themes/${safeTheme}/bootstrap-min.css`;
            // Save selected theme
            putstore('theme', selectedTheme);
            // Update last 4 themes selected
            var lastThemes = JSON.parse(getstore('lastThemes') || '[]');
            if (!lastThemes.includes(selectedTheme)) {
                if (lastThemes.length >= 4) {
                    lastThemes.pop();
                }
                lastThemes.unshift(selectedTheme);
            } else {
                lastThemes = lastThemes.filter(theme => theme !== selectedTheme);
                lastThemes.unshift(selectedTheme);
            }
            putstore('lastThemes', JSON.stringify(lastThemes));
        }

        function account_createMesh() {
            // Check if we are disallowed from creating a device group
            if ((userinfo.siteadmin != 0xFFFFFFFF) && ((userinfo.siteadmin & 64) != 0)) {
                setModalContent('xxAddAgent', "Nuevo Grupo de Dispositivos", 'This account does not have the rights to create a new device group.');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return false;
            }

            // Remind the user to verify the email address
            if ((userinfo.emailVerified !== true) && (serverinfo.emailcheck == true) && (userinfo.siteadmin != 0xFFFFFFFF)) {
                setModalContent('xxAddAgent', "Seguridad de la Cuenta", 'Unable to access this feature until a email address is verified. This is required for password recovery. Go to the "My Account" tab to change and verify an email address.');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return false;
            }

            // Remind the user to add two-factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0)) {
                setModalContent('xxAddAgent', "Seguridad de la Cuenta", 'Unable to access this feature until two-factor authentication is enabled. This is required for extra security. Go to the "My Account" tab and look at the "Account Security" section.');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return false;
            }

            // Look for all relay devices
            var relayDevices = [];
            if ((features & 2) == 0) {
                for (var i in nodes) {
                    var node = nodes[i];
                    if ((node.mtype == 2) && (node.agent != null) && (GetNodeRights(node) == 0xFFFFFFFF)) {
                        relayDevices.push(node);
                    }
                }
            }

            // Prepare the modal content
            var x = "Crear un nuevo grupo de dispositivos utilizando las siguientes opciones." + '<br /><br />';
            var localGroupType = '';

            x += addHtmlFormFloating("Nombre", '<input id=dp2meshname maxlength=128 placeholder="Name" class=form-control onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) />');
            if ((features & 1) == 0) { localGroupType += '<option value=3>' + "Dispositivos locales, sin agente" + '</option>'; }
            if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=103>' + "No hay dispositivos de agente retransmitidos a través del agente" + '</option>'; }
            if (features2 & 0x10000) {
                if ((features & 1) == 0) { localGroupType += '<option value=4>' + "IP-KVM / Dispositivo de alimentación" + '</option>'; }
                if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=104>' + "IP-KVM / Dispositivo de alimentación retransmitido a través de un agente" + '</option>'; }
            }
            x += addHtmlFormFloating("Tipo", '<select id=dp2meshtype class=form-select onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=2>' + "Administrar usando un agente de software" + '</option><option value=1>' + "Sólo Intel&reg; AMT, sin agente" + '</option>' + localGroupType + '</select>');
            if (relayDevices.length > 0) {
                x += '<div id=d2devrelaydiv style=display:none>';
                relayDevices.sort(nameSort);
                var relayDevices2 = [];
                for (var i in relayDevices) { relayDevices2.push('<option value="' + relayDevices[i]._id + '">' + EscapeHtml(relayDevices[i].name) + ' (' + EscapeHtml(relayDevices[i].meshnamel) + ')' + '</option>'); }
                x += addHtmlFormFloating("Dispositivo de Retransmisión", '<select id=d2devrelay onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select >' + relayDevices2.join('') + '</select>');
                x += '</div>';
            }
            x += addHtmlFormFloating("Descripción", '<textarea id=dp2meshdesc maxlength=1024 class=form-control></textarea>');
            x += '<div id=d2ipkvm style=display:none><hr />';
            x += addHtmlFormFloating("Modelo", '<select id=dp2ipkvmmodel onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select ><option value=1>' + "Raritan Dominio KX III" + '</option><option value=2>' + "Interruptor de encendido web 7" + '</option></select>');
            x += addHtmlFormFloating("Nombre de Host", '<input id=dp2ipkvmhost maxlength=128 placeholder="Hostname" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Nombre de Usuario", '<input id=dp2ipkvmuser maxlength=128 placeholder="Username" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Contraseña", '<input id=dp2ipkvmpass type=password placeholder="Password" maxlength=128 onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class="form-control" />');
            x += '</div>';

            // Inject content into the modal
            setModalContent('xxAddAgent', "Nuevo Grupo de Dispositivos", x);

            // Show the modal
            showModal('xxAddAgentModal', 'idx_dlgOkButton', account_createMeshEx);

            // Validate the mesh create form
            account_validateMeshCreate();
            document.getElementById('dp2meshname').focus();
            return false;
        }

        function account_validateMeshCreate(e, x) {
            var meshtype = parseInt(Q('dp2meshtype').value);
            if ((x == 1) && (e != null) && (e.key == "Enter") && (Q('dp2meshname').value.length > 0)) { Q('dp2meshtype').focus(); }
            if ((x == 2) && (e != null) && (e.key == "Enter")) { Q('dp2meshdesc').focus(); }
            var ok = (Q('dp2meshname').value.length > 0);
            QV('d2ipkvm', (meshtype == 4) || (meshtype == 104));
            try { QV('d2devrelaydiv', meshtype > 100); } catch (ex) { }
            if (meshtype == 4) { if ((Q('dp2ipkvmhost').value.length == 0) && (Q('dp2ipkvmuser').value.length == 0) && (Q('dp2ipkvmpass').value.length == 0)) { ok = false; } }
            QE('idx_dlgOkButton', ok);
        }

        function account_createMeshEx(button, tag) {
            var meshtype = parseInt(Q('dp2meshtype').value);
            var cmd = { action: 'createmesh', meshname: Q('dp2meshname').value, meshtype: meshtype, desc: Q('dp2meshdesc').value };
            if ((meshtype == 4) || (meshtype == 104)) {
                cmd.kvmmodel = parseInt(Q('dp2ipkvmmodel').value);
                cmd.kvmhost = Q('dp2ipkvmhost').value;
                cmd.kvmuser = Q('dp2ipkvmuser').value;
                cmd.kvmpass = Q('dp2ipkvmpass').value;
            }
            if (meshtype > 100) {
                cmd.meshtype = (meshtype - 100);
                cmd.relayid = Q('d2devrelay').value;
            }
            meshserver.send(cmd);
        }

        function account_validateDeleteAccount() {
            QE('account_dlgOkButton', (Q('apassword1').value.length > 0) && (Q('apassword1').value == Q('apassword2').value));
        }

        function account_validateNewPassword() {
            var r = '', ok = (Q('apassword0').value.length > 0) && (Q('apassword1').value.length > 0) && (Q('apassword1').value == Q('apassword2').value) && (Q('apassword0').value != Q('apassword1').value);
            if ((features & 0x00010000) && (Q('apasswordhint').value == Q('apassword1').value)) { ok = false; }
            if (Q('apassword1').value != '') {
                if (passRequirements == null || passRequirements == '') {
                    // No password requirements, display password strength
                    var passStrength = checkPasswordStrength(Q('apassword1').value);
                    if (passStrength >= 80) { r = '<span style=color:green>' + "Fuerte" + '<span>'; } else if (passStrength >= 60) { r = '<span style=color:blue>' + "Bueno" + '<span>'; } else { r = '<span style=color:red>' + "Débil" + '<span>'; }
                } else {
                    // Password requirements provided, use that
                    var passReq = checkPasswordRequirements(Q('apassword1').value, passRequirements);
                    if (passReq == false) { ok = false; r = '<span style=color:red>' + "Política" + '<span>' }
                }
            }
            QH('dxPassWarn', r);
            //QE('account_dlgOkButton', ok);
            QE('idx_dlgOkButton', ok);
        }

        // Return a password strength score
        function checkPasswordStrength(password) {
            var r = 0, letters = {}, varCount = 0, variations = { digits: /\d/.test(password), lower: /[a-z]/.test(password), upper: /[A-Z]/.test(password), nonWords: /\W/.test(password) }
            if (!password) return 0;
            for (var i = 0; i < password.length; i++) { letters[password[i]] = (letters[password[i]] || 0) + 1; r += 5.0 / letters[password[i]]; }
            for (var c in variations) { varCount += (variations[c] == true) ? 1 : 0; }
            return parseInt(r + (varCount - 1) * 10);
        }

        // Check password requirements
        function checkPasswordRequirements(password, requirements) {
            if ((requirements == null) || (requirements == '') || (typeof requirements != 'object')) return true;
            if (requirements.min) { if (password.length < requirements.min) return false; }
            if (requirements.max) { if (password.length > requirements.max) return false; }
            var numeric = 0, lower = 0, upper = 0, nonalpha = 0;
            for (var i = 0; i < password.length; i++) {
                if (/\d/.test(password[i])) { numeric++; }
                if (/[a-z]/.test(password[i])) { lower++; }
                if (/[A-Z]/.test(password[i])) { upper++; }
                if (/\W/.test(password[i])) { nonalpha++; }
            }
            if (requirements.numeric && (numeric < requirements.numeric)) return false;
            if (requirements.lower && (lower < requirements.lower)) return false;
            if (requirements.upper && (upper < requirements.upper)) return false;
            if (requirements.nonalpha && (nonalpha < requirements.nonalpha)) return false;
            return true;
        }

        function updateMeshes() {
            var r = '<div class="row">', c = 0, count = 0;

            // Sort the device groups
            var sortedMeshes = [];
            for (i in meshes) { sortedMeshes.push(meshes[i]); }
            sortedMeshes.sort(nameSort);

            for (i in sortedMeshes) {
                // Mesh positioning
                if (c > 1) { r += '</tr><tr>'; c = 0; }
                c++;
                count++;

                // Mesh rights
                var meshrights = GetMeshRights(sortedMeshes[i]);
                var rights = "Permisos Parciales";
                if (meshrights == 0xFFFFFFFF) rights = "Administrador Total"; else if (meshrights == 0) rights = "Sin Permisos";

                // Print the mesh information
                r += `
                    <div class="col-sm-12 col-md-6 col-lg-4 col-xl-3 mb-2"
                            onclick="gotoMesh('` + sortedMeshes[i]._id + `')"
                            onkeypress="if (event.key=='Enter') gotoMesh('` + sortedMeshes[i]._id + `')"
                            style="cursor:pointer;">
                        <div class="card mb-3">
                            <div class="row g-0">
                                <div class="col-2 parent" style="display: flex; justify-content: center; align-items: center;">
                                    <i class="mi me-1"></i>
                                </div>
                                <div class="col-10 mx-auto" tabindex="0">
                                    <div class="card-body" style=padding:0;>
                                        <h5 class="card-title e1">` + EscapeHtml(sortedMeshes[i].name) + `</h5>
                                        <p class="card-text">` + rights + `</p>
                                        <div class="g2"></div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    `;
            }
            r += '</div>';

            meshcount = count;
            QH('p2meshes', r);
            QV('p2noMeshFound', count == 0);
        }

        function updateLoginTokens() {
            var x = '', count = 1;
            if ((loginTokens != null) && (loginTokens.length > 0)) {
                x += '<p><strong>' + "Tokens de inicio de sesión activos" + '</strong> - <span id="p2createMeshLink1"> <button class="btn btn-primary btn-sm" onclick="return account_createLoginToken()"><i class="fa-fw fa-solid fa-circle-plus"></i> ' + "Nuevo" + '</button></span></p>';
                x += '<div style=margin-left:40px><table class="table table-hover table-striped"><tbody><tr class="table-active"><th scope=col style=text-align:left;width:430px>' + "Nombre" + '</th><th scope=col style=text-align:left>' + "Nombre de Usuario" + '</th></tr>';
                for (var i = 0; i < loginTokens.length; i++) {
                    var ltoken = loginTokens[i];
                    var trash = '<a href=# onclick=\'return p2removeLoginToken(event,"' + encodeURIComponentEx(ltoken.tokenUser) + '")\' title="' + "Quitar el token de inicio de sesión" + '" role=button><i class="fa-solid fa-trash text-danger fa-xs"></i></a>';
                    var details = '';
                    if (ltoken.expire != 0) { details = EscapeHtml(format("Caduca el {0}", printDateTime(new Date(ltoken.expire)))) + ' '; }
                    x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%><i class="fa-fw fa-solid fa-user"></i>&nbsp;' + EscapeHtml(ltoken.name) + '<div></div></div></td><td style=width:70%><div style=float:right>' + details + trash + '</div><div>' + EscapeHtml(ltoken.tokenUser) + '</div></td></tr>';
                }
                x += '</tbody></table></div><br />';
                QV('accountCreateLoginTokenSpan', false);
            } else {
                QV('accountCreateLoginTokenSpan', features2 & 0x00000080);
            }
            QH('p2logintokens', x);
        }

        function p2removeLoginToken(e, tokenUser) {
            tokenUser = decodeURIComponent(tokenUser);
            if (loginTokens == null) return;
            var token = null;
            for (var i = 0; i < loginTokens.length; i++) { if (loginTokens[i].tokenUser == tokenUser) { token = loginTokens[i]; } }
            if (token == null) return;
            var x = "¿Confirmar la eliminación de este token de inicio de sesión?" + '<br /><br />';
            x += addHtmlValue("Nombre", EscapeHtml(token.name));
            x += addHtmlValue("Nombre de Usuario", EscapeHtml(token.tokenUser));
            setModalContent('xxAddAgent', "Eliminar Token de Inicio de Sesión", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'loginTokens', remove: [tokenUser] }); });
        }

        function gotoMesh(meshid) {
            currentMesh = meshes[meshid];
            p20updateMesh();
            go(20);
            return false;
        }

        function server_setupGoogleDriveBackup() {
            if (xxdialogMode || (miscState['googleDrive'] == null)) return false;
            var gd = miscState['googleDrive'];
            if (gd.state < 3) {
                var x = '<img style=float:right src="images/googledrive-48.png" /><div>' + "Configura este servidor para que automáticamente cargue las copias de seguridad a Google Drive. Empieza creando e ingresando un ID de Cliente de Google Drive y un Secreto de Cliente para tu cuenta." + '</div><br />';
                x += addHtmlValue("Credenciales", '<a href="https://console.developers.google.com/apis/api/drive.googleapis.com/credentials" rel="noreferrer noopener" target="_blank">' + "Consola de Google Drive" + '</a>');
                x += addHtmlFormFloating("ID del Cliente", '<input id=gdclientid class="form-control" placeholder="xxxxxxxx.apps.googleusercontent.com" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                x += addHtmlFormFloating("Secreto del Cliente", '<input id=gdclientsecret class="form-control" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                setModalContent('xxAddAgent', "Resguardo en Google Drive", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_setupGoogleDriveBackupEx(3, 'gd1'));
                Q('gdclientid').focus();
                QE('idx_dlgOkButton', false);
            } else if (gd.state == 3) {
                var x = '<img style=float:right src="images/googledrive-48.png" /><div>' + "El resguardo en Google Drive está actualmente activo." + '</div>';
                x += '<br /><label><input id=gdcheck type=checkbox class="form-check-input me-2" onchange=server_setupGoogleDriveBackupCheck2() />' + "Remover Configuración" + '</label>';
                setModalContent('xxAddAgent', "Resguardo en Google Drive", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_setupGoogleDriveBackupEx(3, 'gd0'));
                QE('idx_dlgOkButton', false);
            }
        }

        function server_setupGoogleDriveBackupCheck1() { QE('idx_dlgOkButton', (Q('gdclientid').value.length > 0) && (Q('gdclientsecret').value.length > 0)); }
        function server_setupGoogleDriveBackupCheck2() { QE('idx_dlgOkButton', Q('gdcheck').checked); }
        function server_setupGoogleDriveBackupCheck3() { QE('idx_dlgOkButton', Q('gdcode').value.length > 0); }

        function server_setupGoogleDriveBackupEx(b, t) {
            if (t == 'gd0') { meshserver.send({ action: 'serverBackup', service: 'googleDrive', state: 0 }); }
            if (t == 'gd1') { meshserver.send({ action: 'serverBackup', service: 'googleDrive', state: 1, clientid: Q('gdclientid').value, clientsecret: Q('gdclientsecret').value }); }
            if (t == 'gd2') { meshserver.send({ action: 'serverBackup', service: 'googleDrive', state: 2, code: Q('gdcode').value }); }
        }

        function server_showBackupDlg() {
            if (xxdialogMode) return false;
            // check for password in config.json and pass it to server_showBackupDlgUpdate
            meshserver.send({ action: 'serverbackuppassword', check: true, dialog: 'backup' });
            return false;
        }
        function server_showBackupDlgUpdate(configPassword) {
            var x = "Ingrese una contraseña opcional para la copia de seguridad" + '<br /><br />';
            x += '<tr><td align=right>' + nobreak("Contraseña: ") + '</td><td><input id=dbackup_password style=width:256px maxlength=256 type=password name=backup_password autocomplete=off/> <b></b></td></tr>';
            x += '<div><label><input id=doverrulepw type=checkbox ' + (configPassword?'':'disabled') + ' /> ' + "Usar contraseña del sistema" + '</label></div>';
            setModalContent('xxAddAgent', "Descargar copia de seguridad del servidor", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => backup_enterPassword());
            Q('dbackup_password').focus();
            return false;
        }
        function backup_enterPassword() {
            xxModal.hide();
            meshserver.send({ action: 'serverbackuppassword', check: false, password: Q('dbackup_password').value, override: Q('doverrulepw').checked });
            window.location.href = '{{{domainurl}}}backup.zip' + ((urlargs.key)?('?key=' + urlargs.key):'');
        }

        function server_showRestoreDlg() {
            if (xxdialogMode) return false;
            // check for password in config.json and pass it to server_showRestoreDlgUpdate
            meshserver.send({ action: 'serverbackuppassword', check: true, dialog: 'restore' });
            return false;
        }
        function server_showRestoreDlgUpdate(configPassword) {
            var x = "Restaura el servidor utilizando una copia de seguridad," + ' <span style=color:red>' + "esto eliminará los datos del servidor existentes." + '</span> ' + "Sólo haz esto si sabes lo que estás haciendo." + '<br /><br />';
            x += '<div>';
            x += '<input id=account_dlgFileInput type=file name=datafile class="form-control" accept=".zip,application/octet-stream,application/zip,application/x-zip,application/x-zip-compressed" onchange=account_validateServerRestore()>';
            x += '<tr><br><td align=right>' + nobreak("Contraseña de copia de seguridad: ") + '</td><td><input id=drestore_password style=width:256px maxlength=256 type=password name=restore_password autocomplete=off/> <b></b></td></tr>';
            x += '</div>';
            x += '<div><label><input id=drestoreoverrulepw type=checkbox ' + (configPassword?'':'disabled') + ' /> ' + "Usar contraseña del sistema" + '</label></div>';
            setModalContent('xxAddAgent', "Restaurar Servidor", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_uploadServerRestore());
            account_validateServerRestore();
            return false;
        }

        function account_validateServerRestore() {
            QE('idx_dlgOkButton', Q('account_dlgFileInput').files.length == 1);
        }

        function server_uploadServerRestore() {
            meshserver.send({ action: 'serverbackuppassword', check: false, password: Q('drestore_password').value, override: Q('drestoreoverrulepw').checked });
            let formData = new FormData();           
            formData.append('datafile', Q('account_dlgFileInput').files[0]);
            // formData.append('restore_password', Q('drestore_password').value);
            formData.append('auth', authCookie);
            setModalContent('xxAddAgent', "Restaurar Servidor", "Subiendo copia de seguridad");
            QE('idx_dlgOkButton', false);
            QE('idx_dlgCancelButton', false);
            fetch('{{{domainurl}}}restoreserver.ashx' + ((urlargs.key) ? ('?key=' + urlargs.key) : ''), {
                method: 'POST',
                body: formData,
            })
                .then((response) => {
                    if (!response.ok) {
                        setModalContent('xxAddAgent', "Restaurar Servidor", "Error de red: " + response.status);
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        QE('idx_dlgOkButton', true);
                    }
                })
                .catch((error) => {
                    setModalContent('xxAddAgent', "Restaurar Servidor", "Error: " + error.message);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    QE('idx_dlgOkButton', true);
                })
            return false;
        }

        function server_showVersionDlg() {
            if (xxdialogMode) return false;
            xxdialogMode = 2; xxdialogTag = 'MeshCentralServerUpdate';
            setModalContent('xxAddAgent', "Versión de MeshCentral", "Cargando...");
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            meshserver.send({ action: 'serverversion' });
            return false;
        }

        function server_showVersionDlgUpdate() {
            var stableCheck = Q('d2updateCheck1').checked;
            var latestCheck = Q('d2updateCheck2').checked;
            QE('d2updateCheck1', ((xxdialogTag.stable != "Desconocido") && (xxdialogTag.current != xxdialogTag.stable) && (latestCheck == false)));
            QE('d2updateCheck2', ((xxdialogTag.latest != "Desconocido") && (xxdialogTag.current != xxdialogTag.latest) && (stableCheck == false)));
            QE('idx_dlgOkButton', ((stableCheck) && (!latestCheck)) || ((!stableCheck) && (latestCheck)));
        }

        function server_showVersionDlgEx(b, tags) {
            if (Q('d2updateCheck1').checked) { meshserver.send({ action: 'serverupdate', tag: 'stable', version: tags.stable }); }
            if (Q('d2updateCheck2').checked) { meshserver.send({ action: 'serverupdate', tag: 'latest', version: tags.latest }); }
        }

        function server_showErrorsDlg() {
            if (xxdialogMode) return false;
            xxdialogMode = 2; xxdialogTag = 'MeshCentralServerErrors';
            setModalContent('xxAddAgent', "Server Errors", "Cargando...");
            meshserver.send({ action: 'servererrors' });
            return false;
        }
        function server_showErrorsDlgUpdate() { QE('idx_dlgOkButton', Q('d2clearErrorsCheck').checked); }
        function server_showErrorsDlgEx() { meshserver.send({ action: 'serverclearerrorlog' }); }
        function d2CopyServerErrorsToClip() { saveAs(new Blob([Q('d2ServerErrorsLogPre').innerText], { type: 'application/octet-stream' }), "servererrors.txt"); }

        function server_showConfigDlg() {
            if (xxdialogMode) return false;
            xxdialogMode = 2; xxdialogTag = 'MeshCentralServerConfig';
            meshserver.send({ action: 'serverconfig' });
            return false;
        }
        function d2CopyServerConfigToClip() { saveAs(new Blob([Q('d2ServerConfigPre').innerText], { type: 'application/octet-stream' }), 'config.json'); }

        //
        // MY MESHS
        //

        var currentMesh;
        function p20updateMesh() {
            if (currentMesh == null) return;

            // Add device group name
            var meshrights = GetMeshRights(currentMesh), mname = ' - ' + EscapeHtml(currentMesh.name);
            if (mname.length == 0) { mname = '<i>' + "Ninguno" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span role=button tabindex=0 title="' + "Haz clic aquí para editar el nombre del grupo de dispositivos" + '" onclick=p20editmesh(1) onkeyup="if (event.key == \'Enter\') p20editmesh(1)">' + mname + ' <i class="fa-solid fa-pencil fa-2xs"></i></span>'; }
            QH('p20meshName', mname);
            QV('MeshSummary', (currentMesh.mtype != 4));

            var meshtype = format("#{0} desconocido", currentMesh.mtype);
            if (currentMesh.mtype == 1) meshtype = "Sólo Intel&reg; AMT, sin agente";
            if (currentMesh.mtype == 2) meshtype = "Administrado usando un agente de software";
            if (currentMesh.mtype == 3) { if (currentMesh.relayid == null) { meshtype = "Dispositivos locales, sin agente"; } else { meshtype = "No hay dispositivos de agente retransmitidos a través del agente"; } }
            if (currentMesh.mtype == 4) { if (currentMesh.relayid == null) { meshtype = "Dispositivo IP-KVM"; } else { meshtype = "Dispositivo IP-KVM retransmitido a través del agente"; } if (currentMesh.kvm.model == 1) { meshtype += ', ' + 'Raritan KX III'; } }

            var x = '';
            if ((args.hide & 8) != 0) { x += addHtmlValue("Nombre", mname); } // If title bar is hidden, display the mesh name here
            x += addHtmlValue("Descripción", addLinkConditional(((currentMesh.desc && currentMesh.desc != '') ? EscapeHtml(currentMesh.desc) : ('<i>' + "Ninguno" + '</i>')), 'p20editmesh(2)', (meshrights & 1) != 0));

            // Display group type
            x += addHtmlValue("Tipo", meshtype);
            //x += addHtmlValue('Identifier', currentMesh._id.split('/')[2]);

            // Display the relay device if applicable
            if (((currentMesh.mtype == 3) || (currentMesh.mtype == 4)) && (currentMesh.relayid != null)) {
                var relayName = '<i>' + "Desconocido" + '</i>';
                var relayNode = getNodeFromId(currentMesh.relayid);
                if (relayNode != null) { relayName = EscapeHtml(relayNode.name) + ' (' + EscapeHtml(relayNode.meshnamel) + ')'; }
                x += addHtmlValue("Dispositivo de Retransmisión", addLinkConditional(relayName, 'p20editmeshrelay()', (meshrights & 1) != 0));
            }

            // Display IP-KVM information if needed
            if (currentMesh.mtype == 4) {
                x += addHtmlValue("Nombre de Host", currentMesh.kvm.host);
                x += addHtmlValue("Nombre de Usuario", currentMesh.kvm.user);
            }

            // Display device group creator
            if ((currentMesh.creatorid != null) && (users != null) && (users[currentMesh.creatorid] != null)) {
                var meshcreator = users[currentMesh.creatorid];
                x += addHtmlValue("Creado por", '<a href=# onclick=gotoUser(\"' + encodeURIComponentEx(meshcreator._id) + '\",false,null)>' + EscapeHtml(meshcreator.name) + '</a>');
            } else if (currentMesh.creatorname != null) {
                x += addHtmlValue("Creado por", EscapeHtml(currentMesh.creatorname));
            }

            // Display creation time
            if (currentMesh.creation != null) { x += addHtmlValue("Fecha de Creación", printDateTime(new Date(currentMesh.creation))); }

            // Display features
            if (currentMesh.mtype != 3) {
                var meshFeatures = [];
                if (currentMesh.flags) {
                    if (currentMesh.flags & 1) { meshFeatures.push("Auto-Remover"); }
                    if (currentMesh.flags & 2) { meshFeatures.push((currentMesh.mtype == 4) ? "Sincronización de nombre de puerto" : "Sincronización de Nombre de Host"); }
                    if (currentMesh.flags & 8) { meshFeatures.push("preferir --agentname"); }
                    if (currentMesh.flags & 16) { meshFeatures.push("permitir anulación"); }
                    if ((serverinfo.devGroupSessionRecording == 1) && (currentMesh.flags & 4)) { meshFeatures.push("Grabar Sesiones"); }
                }
                if ((typeof currentMesh.expireDevs == 'number') && (currentMesh.expireDevs > 0)) { meshFeatures.push("Eliminar inactivo"); }
                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Ninguno" + '</i>'; }
                x += addHtmlValue("Caracteristicas", addLinkConditional(meshFeatures, 'p20editmeshfeatures()', meshrights & 1));
            }

            // Display device group user consent
            if (currentMesh.mtype == 2) {
                var meshFeatures = [];
                var consent = 0;
                if (currentMesh.consent) { consent = currentMesh.consent; }
                if (serverinfo.consent) { consent |= serverinfo.consent; }
                if ((consent & 0x0040) && (consent & 0x0008)) { meshFeatures.push("Mensaje de Escritorio + Barra de Herramientas"); } else if (consent & 0x0040) { meshFeatures.push("Barra de Herramientas de Escritorio"); } else if (consent & 0x0008) { meshFeatures.push("Mensaje de Escritorio"); } else { if (consent & 0x0001) { meshFeatures.push("Notificación de Escritorio"); } }
                if (consent & 0x0010) { meshFeatures.push("Mensaje en Terminal"); } else { if (consent & 0x0002) { meshFeatures.push("Notificación de Terminal"); } }
                if (consent & 0x0020) { meshFeatures.push("Solicitud de Archivos"); } else { if (consent & 0x0004) { meshFeatures.push("Notificación de Archivos"); } }
                if (consent == 7) { meshFeatures = ["Siempre Notificar"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Siempre Preguntar"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Ninguno" + '</i>'; }
                x += addHtmlValue("Consentimiento del Usuario", addLinkConditional(meshFeatures, 'p20editmeshconsent(1)', meshrights & 1));
            }

            if (currentMesh.mtype != 3) {
                if ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 1024) == 0)) {
                    // Display user notification
                    var meshNotify = 0, meshNotifyStr = [];
                    if (userinfo.links && userinfo.links[currentMesh._id] && userinfo.links[currentMesh._id].notify) { meshNotify |= userinfo.links[currentMesh._id].notify; }
                    if (userinfo.notify && userinfo.notify[currentMesh._id]) { meshNotify |= userinfo.notify[currentMesh._id]; }
                    if (meshNotify & 2) { meshNotifyStr.push("Conectar"); }
                    if (meshNotify & 4) { meshNotifyStr.push("Desconectar"); }
                    if (meshNotify & 8) { meshNotifyStr.push("Intel&reg; AMT"); }
                    if ((features2 & 0x00004000) && (userinfo.emailVerified)) {
                        var xx = 0;
                        if (meshNotify & 16) { xx++; } if (meshNotify & 32) { xx++; } if (meshNotify & 64) { xx++; }
                        if (xx > 0) { meshNotifyStr.push(format("Correo Electronico ({0})", xx)); }
                    }
                    if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                        var xx = 0;
                        if (meshNotify & 128) { xx++; } if (meshNotify & 256) { xx++; } if (meshNotify & 512) { xx++; }
                        if (xx > 0) { meshNotifyStr.push(format("Mensajería ({0})", xx)); }
                    }
                    if (meshNotifyStr.length == 0) { meshNotifyStr.push('<i>' + "Ninguno" + '</i>'); }
                    x += addHtmlValue("Notificaciones", addLink(meshNotifyStr.join(', '), 'p20editMeshNotify()'));
                }
            }

            // Display invitation codes
            if ((features & 0x01000000) && (currentMesh.mtype == 2)) {
                var inviteCodeStr = '<i>' + "Ninguno" + '</i>', icodes = false;
                if (currentMesh.invite != null) { icodes = true; inviteCodeStr = currentMesh.invite.codes.join(', '); /* + ', ' + currentMesh.invite.flags;*/ }
                x += addHtmlValue("Códigos de Invitación", addLinkConditional(inviteCodeStr, 'p20editmeshInviteCode()', (meshrights & 1) || (icodes)));
            }

            // If the Intel AMT manager is active on the server, show the Intel AMT policy edit box.
            if ((currentMesh.mtype < 3) && ((features2 & 1) != 0)) {
                // Intel AMT setup
                var intelAmtPolicy = "Ninguna Política";
                if (currentMesh.amt) {
                    if (currentMesh.amt.type == 1) { intelAmtPolicy = "Desactivar"; }
                    else if (currentMesh.amt.type == 2) {
                        intelAmtPolicy = "Modo de Control de Cliente (CCM) simple";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 3) {
                        intelAmtPolicy = "Modo de Control de Administrador (ACM) simple";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 4) {
                        intelAmtPolicy = "Completamente automatico";
                    }
                }
                x += addHtmlValue("Intel&reg; AMT", addLinkConditional(intelAmtPolicy, 'p20editMeshAmt()', meshrights & 1));
            }

            QH('p20info3', x);
            x = '';

            // Display device group note support
            if (meshrights & 1) { x += '<div class="row py-1"><div class="col"><input type=button class="btn btn-primary btn-sm mb-1" value=' + "Notas" + ' title="' + "Ver notas sobre este grupo de dispositivos" + '" onclick=showNotes(false,"' + encodeURIComponentEx(currentMesh._id) + '") /></div></div>'; }
            if (meshrights & 2) {
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick="return p20showAddMeshUserDialog()"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Usuarios" + '</button>';
                if (usergroups != null) {
                    var userGroupCount = 0, newUserGroup = false;
                    for (var i in usergroups) {
                        if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != currentMesh._id.split('/')[1])) continue;
                        userGroupCount++;
                        if ((currentMesh.links == null) || (currentMesh.links[i] == null)) { newUserGroup = true; }
                    }
                    if ((userGroupCount > 0) && (newUserGroup)) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick="return p20showAddMeshUserDialog(2)"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Grupo de Usuarios" + '</button>'; }
                }
            }

            //if (meshrights & 4) { }
            if (currentMesh.mtype == 1) {
                if (meshrights & 1) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" title="' + "Importar dispositivos Intel&reg; AMT." + '" onclick=\'return showAmtImport("' + currentMesh._id + '")\'><i class="fa-solid fa-circle-arrow-down"></i> ' + "Importar" + '</button>'; }
                /*
                if ((features & 1) == 0) { // If not WAN-Only
                    x += '<a href=# onclick=\'return addDeviceToMesh("' + currentMesh._id + '")\' style=cursor:pointer;margin-right:10px title="' + "Add a new Intel&reg; AMT computer that is located on the local network." + '"><img src=images/icon-installmesh.png border=0 height=12 width=12> ' + "Install local" + '</a>';
                }
                */
                if ((currentMesh.amt != null) && (currentMesh.amt.type > 0)) { // CCM Deactivate, CCM or ACM activation
                    x += '<button class="btn btn-primary btn-sm me-2 mb-1" title="' + "Realizar la activación y configuración de Intel&reg; AMT." + '" onclick=\'return showAmtSetup("' + currentMesh._id + '")\'><i class="fa-solid fa-gear"></i> ' + "Preparar" + '</button>';
                }
            }
            if ((currentMesh.mtype == 2) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) {
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick=\'return addAgentToMesh("' + currentMesh._id + '")\' title="' + "Agrega una nueva computadora a este grupo de dispositivos instalando el Agente Mesh." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Agente" + '</button>';
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick=\'return inviteAgentToMesh("' + currentMesh._id + '")\' title="' + "Invita a alguien a instalar el Agente Mesh en este grupo de dispositivos." + '"><i class="fa-solid fa-user-plus"></i> ' + "Invitar" + '</button>';
            }
            if ((currentMesh.mtype == 3) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) {
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick=\'return addLocalDeviceToMesh("' + currentMesh._id + '")\' title="' + "Agrega un dispositivo ubicado en la red local." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Dispositivo" + '</button>';
            }
            if (currentMesh.amt && (currentMesh.amt.type > 2) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { // ACM activation or Full Automatic
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" title="' + "Cambiar Intel AMT al Modo de Control de Administrador (ACM)." + '" onclick=\'return showAmtAcmSetup()\'><i class="fa-solid fa-circle-arrow-down"></i> ' + "ACM" + '</button>';
            }

            x += '<table class="table table-hover table-striped" cellpadding=2 cellspacing=0><tbody><tr><th scope=col>' + "Autorizaciones de Usuario" + '</th><th scope=col></th></tr>';

            // Sort the users for this mesh
            var count = 1, sortedusers = [];
            for (var i in currentMesh.links) {
                var uname = i.split('/')[2];
                if (currentMesh.links[i].name) { uname = currentMesh.links[i].name; }
                if (i == userinfo._id) { uname = userinfo.name; }
                if ((usergroups != null) && (usergroups[i] != null)) { uname = usergroups[i].name; }
                sortedusers.push({ id: i, name: uname, rights: currentMesh.links[i].rights });
            }
            sortedusers.sort(function (a, b) { if (a.name > b.name) return 1; if (a.name < b.name) return -1; return 0; });

            // Display all users for this device group
            for (var i in sortedusers) {
                var trash = '', r = sortedusers[i].rights, rights = makeDeviceGroupRightsString(r), icon = 'fa-user';
                if ((sortedusers[i].id != userinfo._id) && (meshrights == 0xFFFFFFFF || (((meshrights & 2) != 0)))) {
                    if ((meshrights == 0xFFFFFFFF) || (currentMesh.links[sortedusers[i].id].rights != 0xFFFFFFFF)) {
                        trash = '<a href=# onclick=\'return p20deleteUser(event,"' + encodeURIComponentEx(sortedusers[i].id) + '")\' title="' + "Remover permisos de usuario de este grupo de dispositivos" + '"><i role=button class="fa-solid fa-trash text-danger fa-fw fa-xs"></i></a>';
                    }
                    rights = '<span tabindex=0 style=cursor:pointer onclick=p20viewuser("' + encodeURIComponentEx(sortedusers[i].id) + '") onkeypress="if (event.key==\'Enter\') p20viewuser(\'' + encodeURIComponentEx(sortedusers[i].id) + '\')">' + rights + ' <i class="fa-fw fa-solid fa-pencil fa-xs"></i></span>';
                }
                if (sortedusers[i].id.startsWith('ugrp/')) { icon = 'fa-users'; }
                var username = EscapeHtml(decodeURIComponent(sortedusers[i].name));
                if ((usergroups != null) && sortedusers[i].id.startsWith('ugrp/')) { username = '<a tabindex=0 href=# onclick=\'gotoUserGroup("' + encodeURIComponentEx(sortedusers[i].id) + '");haltEvent(event);\'>' + username + '</a>'; }
                if ((users != null) && sortedusers[i].id.startsWith('user/')) { username = '<a tabindex=0 href=# onclick=\'gotoUser("' + encodeURIComponentEx(sortedusers[i].id) + '");haltEvent(event);\'>' + username + '</a>'; }
                x += '<tr style=' + (((count % 2) == 0) ? ';background-color:#DDD' : '') + '><td style=width:30%><div><i title="' + "Usuario" + '" class="fa-solid fa-fw ' + icon + '"></i>&nbsp;' + username + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
                ++count;
            }

            x += '</tbody></table>';

            // Show device shares
            if ((deviceShares != null) && (deviceSharesNode == currentMesh._id) && (deviceShares.length > 0)) {
                x += '<p></p><table class="table table-hover table-striped" cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col style=text-align:left;width:430px>' + "Compartir dispositivo activo" + '</th><th scope=col style=text-align:left><th scope=col style=text-align:left></th></tr>';
                count = 1;
                for (var i = 0; i < deviceShares.length; i++) {
                    var dshare = deviceShares[i], trash = '';
                    if (dshare.url != null) { trash += '<a href="' + dshare.url + '" rel="noreferrer noopener" target=_blank title="' + "Enlace para compartir dispositivo" + '" style=cursor:pointer><img src=images/link2.png border=0 height=10 width=10></a> '; }
                    trash += '<a href=# onclick=\'return p30removeDeviceSharing(event,"' + encodeURIComponentEx(dshare.nodeid) + '","' + encodeURIComponentEx(dshare.publicid) + '","' + encodeURIComponentEx(dshare.guestName) + '")\' title="' + "Eliminar dispositivo compartido" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                    var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Escritorio", "Escritorio + Terminal", "Archivos", "Terminal + Archivos", "Escritorio + Archivos", "Escritorio + Terminal + Archivos"][dshare.p]; } else if (dshare.p == 8) { type = "HTTP/" + dshare.port; } else if (dshare.p == 16) { type = "HTTPS/" + dshare.port; }
                    var details = type;
                    if ((dshare.startTime != null) && (dshare.expireTime != null)) { details = format("{0}, {1} a {2}", type, printFlexDateTime(new Date(dshare.startTime)), printFlexDateTime(new Date(dshare.expireTime))); }
                    if ((dshare.startTime != null) && (dshare.duration != null)) {
                        if (dshare.duration < 2) {
                            details = format("{0}, {1} por {2} minuto", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        } else {
                            details = format("{0}, {1} durante {2} minutos", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        }
                    }
                    if (dshare.recurring == 1) { details += ", Recurrente diariamente"; }
                    if (dshare.recurring == 2) { details += ", Recurrente semanalmente"; }
                    if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Ver solo escritorio"; }
                    if (dshare.consent != null) {
                        if (dshare.consent == 0) { details += ", Sin consentimiento"; } else {
                            if ((dshare.consent & 0x0038) != 0) { details += ", Solicitud de consentimiento"; }
                            if ((dshare.consent & 0x0040) != 0) { details += ", Barra de herramientas"; }
                        }
                    }
                    var guestName = EscapeHtml(dshare.guestName);
                    if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Autocompartido del agente" + '</i>'; }
                    var node = getNodeFromId(dshare.nodeid);
                    if (node != null) {
                        var gray = ((node.conn > 0) ? '' : ' gray');
                        var computer = '<div onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\' style=float:left class="j' + node.icon + gray + '"></div>&nbsp;<a onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\'>' + EscapeHtml(node.name) + '</a>';
                        x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%>' + computer + '<td style=width:30%><div class=m' + 2 + '></div><div>&nbsp;' + guestName + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + details + '</div></td></tr>';
                    }
                }
                x += '</tbody></table>';
            } else {
                // Request device sharing
                if ((deviceSharesNode != currentMesh._id) && (deviceSharesReq != currentMesh._id)) {
                    deviceSharesReq = currentMesh._id;
                    meshserver.send({ action: 'deviceMeshShares', meshid: currentMesh._id });
                }
            }

            // Display list of devices in this device group
            count = 0;
            nodes.sort(deviceSort);
            var dllist = '';
            if (currentMesh.mtype == 1) {
                dllist += '<a onclick=meshDownloadDeviceList()><img title="' + "Descargar lista de dispositivos" + '" src="images/link4.png" /></a>';
                if ((features & 1) == 0) { dllist += ' <a onclick=meshImportDeviceList()><img title="' + "Importar lista de dispositivos" + '" src="images/link6.png" /></a>'; } // Show import only in LAN or Hybrid mode
            }

            var y = '<table class="table table-hover table-striped"><tbody><tr class="class="fw-bold"><th>' + "Dispositivos" + '</th><th scope=col class="text-end">' + dllist + '</th></tr>';
            for (var i in nodes) {
                var node = nodes[i], gray = ((node.conn > 0) ? '' : ' gray');
                if (currentMesh._id != node.meshid) continue;
                y += '<tr><td style=width:30%><div onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\' style=float:left class="j' + node.icon + gray + '"></div>&nbsp;<a onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\'>' + EscapeHtml(node.name) + '</a></div></div></td><td><div style=float:right>' + PowerStateStr(node.pwr) + '&nbsp;</div><div>' + (node.osdesc ? EscapeHtml(node.osdesc) : '') + '</div></td></tr>';
                ++count;
            }
            if (count == 0) { y += '<tr><td colspan="2"><i>' + "Ninguno" + '</i></td></tr>'; }
            y += '</tbody></table>';

            // If we are full administrator on this mesh, allow deletion of the mesh
            if (meshrights == 0xFFFFFFFF) {
                y += '<div style=font-size:small;text-align:right><span><a href=# onclick=p20showDeleteMeshDialog() style=cursor:pointer>' + "Eliminar Grupo de Dispositivos" + '</a></span></div>';
            }

            QH('p20info', x);
            QH('p20info2', y);

            // Change the URL
            var urlviewmode = '';
            if (((features & 0x10000000) == 0) && (xxcurrentView >= 20) && (xxcurrentView <= 29) && (currentMesh != null)) {
                urlviewmode = '?viewmode=' + xxcurrentView + '&gotomesh=' + currentMesh._id.split('/')[2];
                for (var i in urlargs) { urlviewmode += ('&' + i + '=' + urlargs[i]); }
                try { window.history.replaceState({}, document.title, window.location.pathname + urlviewmode); } catch (ex) { }
            }
        }

        // Save the list of devices in a device group in the MeshCommander format
        function meshDownloadDeviceList() {
            if (xxdialogMode) return;
            var r = { webappversion: '0.9.0', computers: [] };
            for (var i in nodes) {
                var node = nodes[i];
                if ((currentMesh._id != node.meshid) || (node.intelamt == null)) continue;
                var c = { name: node.name, host: node.host, user: node.intelamt.user, pass: '', tls: (node.intelamt.tls == 1) ? 1 : 0, ver: node.intelamt.ver, pstate: node.intelamt.state }
                if (node.intelamt.state == 2) { c.pmode = 1; } // TODO
                if (node.intelamt.realm) { c.digestrealm = node.intelamt.realm; }
                if (node.intelamt.hash) { c.tlscerthash = node.intelamt.hash; }
                r.computers.push(c);
            }
            saveAs(stringToUtf8BlobNoHeader(JSON.stringify(r, null, 2)), currentMesh.name + '.json');
        }

        // Import a list of devices into the Intel AMT only device group
        function meshImportDeviceList() {
            if (xxdialogMode) return;
            var x = "Importa una lista local de dispositivos con Intel&reg; AMT en el formato MeshCommander JSON." + '<br /><br /><input type=file id=d4importFile class="form-control" accept=".json" onchange=meshImportDeviceListValidate() />';
            setModalContent('xxAddAgent', "Importar dispositivos Intel&reg; AMT", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => meshImportDeviceListEx());
            QE('idx_dlgOkButton', false);
        }

        function meshImportDeviceListValidate() {
            QE('idx_dlgOkButton', Q('d4importFile').value != null);
        }

        function meshImportDeviceListEx() {
            var fr = new FileReader();
            fr.onload = function (r) {
                var j = null;
                try {
                    j = JSON.parse(r.target.result);
                } catch (ex) {
                    setModalContent('xxAddAgent', "Importar dispositivos Intel&reg; AMT", format("Archivo JSON inválido: {0}.", ex));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    return;
                }
                if ((typeof j == 'object') && (typeof j.webappversion == 'string') && (typeof j.computers == 'object') && (Array.isArray(j.computers))) {
                    var ok = 0;
                    for (var k in j.computers) {
                        var computer = j.computers[k];
                        if ((typeof computer.name == 'string') && (typeof computer.host == 'string') && (typeof computer.user == 'string') && (typeof computer.pass == 'string')) {
                            var cmd = { action: 'addamtdevice', meshid: currentMesh._id, devicename: computer.name, hostname: computer.host, amtusername: computer.user, amtpassword: computer.pass, amttls: (computer.tls == 1) ? 1 : 0 };
                            if (typeof computer.ver == 'string') { cmd.ver = computer.ver; }
                            if (typeof computer.pstate == 'number') { cmd.state = computer.pstate; }
                            if (typeof computer.digestrealm == 'string') { cmd.realm = computer.digestrealm; }
                            if (typeof computer.tlscerthash == 'string') { cmd.hash = computer.tlscerthash; }
                            meshserver.send(cmd);
                            ok++;
                        }
                    }
                    if (ok == 0) {
                        setModalContent('xxAddAgent', "Importar Cuenta de Usuario", 'Unable to import any device.');
                    }
                } else {
                    setModalContent('xxAddAgent', "Importar dispositivos Intel&reg; AMT", 'Invalid JSON file format.');
                }
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            };
            fr.readAsText(Q('d4importFile').files[0]);
        }

        function p20editMeshAmt() {
            if (xxdialogMode) return;
            var x = '', acmoption = '';
            if ((features & 0x100000) != 0) { acmoption = '<option value=3>' + "Modo de Control de Administrador (ACM) simple" + '</option>'; }
            x += addHtmlFormFloating("Tipo", '<select id=dp20amtpolicy class="form-select" onchange=p20editMeshAmtChange()><option value=0>' + "Ninguna Política" + '</option><option value=1>' + "Desactivar" + '</option><option value=2>' + "Modo de Control de Cliente (CCM) simple" + '</option>' + acmoption + '<option value=4>' + "Completamente automatico" + '</option></select>')
            x += '<div id=dp20amtpolicydiv></div>';

            setModalContent('xxAddAgent', "Política de Intel&reg; AMT", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p20editMeshAmtEx);

            if (currentMesh.amt) { Q('dp20amtpolicy').value = currentMesh.amt.type; }
            p20editMeshAmtChange();

            // Set the current Intel AMT policy
            if (currentMesh.amt && ((currentMesh.amt.type == 2) || (currentMesh.amt.type == 3))) {
                Q('dp20amtpolicypass').value = currentMesh.amt.password;
                if ((currentMesh.amt.type == 2) || (currentMesh.amt.type == 3)) {
                    if (currentMesh.amt.badpass != null) { Q('dp20amtbadpass').value = currentMesh.amt.badpass; }
                    if ((currentMesh.amt.type == 3) && (currentMesh.amt.ccm != null)) { Q('dp20amtccmmode').value = currentMesh.amt.ccm; }
                }
                if ((features & 0x400) == 0) { Q('dp20amtcira').value = currentMesh.amt.cirasetup; }
            }

            dp20amtValidatePolicy();
        }

        function p20editMeshAmtChange() {
            var ptype = Q('dp20amtpolicy').value, x = '';
            if ((ptype == 2) || (ptype == 3)) {
                var keeppass = ((currentMesh.amt != null) && (currentMesh.amt.password == 1)) ? '<option value=1 selected>' + "Mantener la contraseña existente" + '</option>' : '';
                x += addHtmlFormFloating("Contraseña", '<select id=dp20amtpass class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Contraseña Aleatoria" + '</option>' + keeppass + '<option value=2>' + "Seleccionar nueva contraseña" + '</option></select>')
                x += '<div id=dp20amtpassdiv style=display:none>';
                x += addHtmlFormFloating("Nueva contraseña*", '<input id=dp20amtpolicypass type=password class="form-control" maxlength=32 placeholder="New password*" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy() autocomplete=off />');
                x += addHtmlFormFloating("Nueva contraseña*", '<input id=dp20amtpolicypass2 type=password class="form-control" maxlength=32 placeholder="New password*" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy() autocomplete=off />');
                x += '</div>';
                if (ptype == 3) { x += addHtmlFormFloating("Modo CCM", '<select id=dp20amtccmmode class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "No cambie, mantenga CCM si está configurado" + '</option><option value=1>' + "Desactivar CCM si está configurado" + '</option><option value=2>' + "Activar a CCM, si ACM falla" + '</option></select>'); }
                x += '<div id=dp20amtbadpassdiv style=display:none>';
                x += addHtmlFormFloating("Contraseña desconocida", '<select id=dp20amtbadpass class="form-select"><option value=0>' + "No hacer nada" + '</option><option value=1>' + "Si está en CCM, reactivar Intel&reg; AMT" + '</option></select>');
                x += '</div>';
                if ((features & 0x400) == 0) { x += addHtmlFormFloating("Configuración de CIRA", '<select id=dp20amtcira class="form-select" title="' + "Acceso Remoto Iniciado por el Cliente (CIRA)" + '"><option value=0>' + "No hacer nada" + '</option><option value=1>' + "No conectar al servidor" + '</option><option value=2>' + "Conectar al servidor" + '</option></select>'); }
                x += '<span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 caracteres, 1 superior, 1 inferior, 1 numérico, 1 no alfanumérico." + '</span>';
                if ((currentMesh.mtype == 2) && (ptype == 2)) { x += '<span style="font-size:10px"> ' + "Esta política no afectará a los dispositivos con Intel&reg; AMT en modo ACM." + '</span>'; }
            }
            if (ptype == 0) { x = '<div class="row"><div class="col-2"><i class="fa-solid fa-times-circle text-danger" style="padding-right:8px; font-size:60px;"></i></div><div class="col-10">' + "Cuando se selecciona esta política, este servidor no administra Intel&reg; AMT. Intel AMT todavía se puede utilizar activándolo y configurándolo manualmente." + '</div></div>'; }
            if (ptype == 1) { x = '<div class="row"><div class="col-2"><i class="fa-solid fa-times-circle text-danger" style="font-size:60px; padding-right:8px; color:green;"></i></div><div class="col-10">' + "Cuando se selecciona esta política, se desactivará cualquier Intel&reg; AMT en el modo de control de cliente (CCM). Otros dispositivos tendrán CIRA borrado y aún se pueden administrar manualmente." + '</div></div>'; }
            if (ptype == 4) { x = '<div class="row"><div class="col-2"><i class="fa-solid fa-check-circle text-success" style="font-size:60px; padding-right:8px; color:blue;"></i></div><div class="col-10">' + "Esta es la política recomendada. La activación y administración de Intel&reg; AMT está completamente automatizada y el servidor intentará hacer el mejor uso posible de la administración de hardware." + '</div></div>'; }
            QH('dp20amtpolicydiv', x);
            setTimeout(dp20amtValidatePolicy, 500);
        }

        function dp20amtValidatePolicy() {
            var ok = true, ptype = Q('dp20amtpolicy').value;
            if (((ptype == 2) || (ptype == 3)) && (Q('dp20amtpass').value == 2)) {
                var pass = Q('dp20amtpolicypass').value, pass2 = Q('dp20amtpolicypass2').value;
                ok = ((pass === pass2) && passwordcheck(pass));
            }
            QE('idx_dlgOkButton', ok);
            if ((ptype == 2) || (ptype == 3)) { QV('dp20amtpassdiv', Q('dp20amtpass').value == 2); }
            QV('dp10passNotify', ((ptype == 2) || (ptype == 3)) && (Q('dp20amtpass').value == 2));
            QV('dp20amtbadpassdiv', (ptype == 2) || ((ptype == 3) && (Q('dp20amtccmmode').value != 1)));
        }

        function p20editMeshAmtEx() {
            var ptype = parseInt(Q('dp20amtpolicy').value), amtpolicy = { type: ptype };
            var password = null;
            if ((ptype == 2) || (ptype == 3)) {
                if (Q('dp20amtpass').value == 0) { password = ''; } // Randomize
                if (Q('dp20amtpass').value == 1) { password = null; } // Keep same
                if (Q('dp20amtpass').value == 2) { password = Q('dp20amtpolicypass').value; } // Set new password
            }
            if (ptype == 2) { // CCM policy
                amtpolicy = { type: ptype, password: password, badpass: parseInt(Q('dp20amtbadpass').value) };
                if ((features & 0x400) == 0) { amtpolicy.cirasetup = parseInt(Q('dp20amtcira').value); } else { amtpolicy.cirasetup = 1; }
            } else if (ptype == 3) { // ACM policy
                amtpolicy = { type: ptype, password: password, badpass: parseInt(Q('dp20amtbadpass').value), ccm: parseInt(Q('dp20amtccmmode').value) };
                if ((features & 0x400) == 0) { amtpolicy.cirasetup = parseInt(Q('dp20amtcira').value); } else { amtpolicy.cirasetup = 1; }
            } else if (ptype == 4) { // Fully automatic policy
                amtpolicy = { type: ptype };
            }
            meshserver.send({ action: 'meshamtpolicy', meshid: currentMesh._id, amtpolicy: amtpolicy });
        }

        function p20showDeleteMeshDialog() {
            if (xxdialogMode) return false;
            var x = format("¿Estás seguro de que desea eliminar el grupo {0}? Al eliminar el grupo de dispositivos también se eliminará toda la información sobre los dispositivos dentro de este grupo.", EscapeHtml(currentMesh.name)) + '<br /><br />';
            x += '<label class=form-check-label><input id=p20check type=checkbox class="form-check-input m-2" onchange=p20validateDeleteMeshDialog() />' + "Confirmar" + '</label>';
            setModalContent('xxAddAgent', "Eliminar Grupo de Dispositivos", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p20showDeleteMeshDialogEx(3, '');
            });
            p20validateDeleteMeshDialog();
            return false;
        }

        function p20validateDeleteMeshDialog() {
            QE('idx_dlgOkButton', Q('p20check').checked);
        }

        function p20showDeleteMeshDialogEx(buttons, tag) {
            meshserver.send({ action: 'deletemesh', meshid: currentMesh._id, meshname: currentMesh.name });
        }

        function p20editmeshrelay() {
            if (xxdialogMode) return;

            // Look for all relay devices
            var relayDevices = [];
            if ((features & 2) == 0) { for (var i in nodes) { var node = nodes[i]; if ((node.mtype == 2) && (node.agent != null) && (GetNodeRights(node) == 0xFFFFFFFF)) { relayDevices.push(node); } } }
            relayDevices.sort(nameSort);

            if (relayDevices.length == 0) {
                // Relay relay devices available
                x = "No hay dispositivos de relé disponibles.";
                setModalContent('xxAddAgent', "Editar Grupo de Dispositivos", "No hay dispositivos de relé disponibles.");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            } else {
                var relayDevices2 = [];
                for (var i in relayDevices) { relayDevices2.push('<option value="' + (relayDevices[i]._id + '"' + ((currentMesh.relayid == relayDevices[i]._id) ? ' selected' : '')) + '>' + EscapeHtml(relayDevices[i].name) + ' (' + EscapeHtml(relayDevices[i].meshnamel) + ')' + '</option>'); }
                var x = addHtmlFormFloating("Dispositivo de Retransmisión", '<select id=d2devrelay class="form-select">' + relayDevices2.join('') + '</select>');
                setModalContent('xxAddAgent', "Editar Grupo de Dispositivos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', p20editmeshrelayEx);
            }
        }

        function p20editmeshrelayEx() {
            meshserver.send({ action: 'editmesh', meshid: currentMesh._id, relayid: Q('d2devrelay').value });
        }

        function p20editmesh(focus) {
            if (xxdialogMode) return;
            var x = '<div class="form-floating mb-2">';
            x += '<input type=text class="form-control" id=dp20meshname maxlength=128 placeholder="' + "Nombre" + '" onchange=p20editmeshValidate() onkeyup=p20editmeshValidate(event) />';
            x += '<label for=dp20meshname>' + "Nombre" + '</label>';
            x += '</div>';

            x += '<div class="form-floating mb-2">';
            x += '<textarea id=dp20meshdesc class="form-control" maxlength=1024 placeholder="' + "Descripción" + '"></textarea>';
            x += '<label for=dp20meshdesc>' + "Descripción" + '</label>';
            x += '</div>';
            setModalContent('xxAddAgent', "Editar Grupo de Dispositivos", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p20editmeshEx);

            Q('dp20meshname').value = currentMesh.name;
            if (currentMesh.desc) Q('dp20meshdesc').value = currentMesh.desc;
            p20editmeshValidate();
            if (focus == 2) { Q('dp20meshdesc').focus(); } else { Q('dp20meshname').focus(); }
        }

        function p20editmeshEx() {
            meshserver.send({ action: 'editmesh', meshid: currentMesh._id, meshname: Q('dp20meshname').value, desc: Q('dp20meshdesc').value });
        }

        function p20editmeshValidate(e) {
            QE('idx_dlgOkButton', Q('dp20meshname').value.length > 0);
            if (e && e.key == 'Enter') { Q('dp20meshdesc').focus(); }
        }

        // editType: 1 = currentMesh, 2 = currentUser, 3 = currentDevice
        function p20editmeshconsent(editType) {
            if (xxdialogMode) return;
            var x = '', consent = 0, title = '';
            if (editType == 1) { consent = (currentMesh.consent) ? currentMesh.consent : 0; title = "Editar Consentimiento del Usuario del Grupo de Dispositivos"; }
            if (editType == 2) { consent = (currentUser.consent) ? currentUser.consent : 0; title = "Editar Consentimiento del Usuario"; }
            if (editType == 3) { consent = (currentNode.consent) ? currentNode.consent : 0; title = "Editar Consentimiento del Usuario del Dispositivo"; }
            if (editType == 4) { consent = (currentUserGroup.consent) ? currentUserGroup.consent : 0; title = "Editar el consentimiento del usuario del grupo de usuarios"; }
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px"><b>' + "Escritorio" + '</b></div>';
            x += '<div><label for=d20flag1><input type=checkbox id=d20flag1 class="form-check-input me-2" ' + ((consent & 0x0001) ? 'checked' : '') + '>' + "Notificar al usuario" + '</label></div>';
            x += '<div><label for=d20flag2><input type=checkbox id=d20flag2 class="form-check-input me-2" ' + ((consent & 0x0008) ? 'checked' : '') + '>' + "Solicitar consentimiento del usuario" + '</label></div>';
            x += '<div><label for=d20flag7><input type=checkbox id=d20flag7 class="form-check-input me-2" ' + ((consent & 0x0040) ? 'checked' : '') + '>' + "Mostrar barra de herramientas de conexión" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Terminal" + '</b></div>';
            x += '<div><label for=d20flag3><input type=checkbox id=d20flag3 class="form-check-input me-2" ' + ((consent & 0x0002) ? 'checked' : '') + '>' + "Notificar al usuario" + '</label></div>';
            x += '<div><label for=d20flag4><input type=checkbox id=d20flag4 class="form-check-input me-2" ' + ((consent & 0x0010) ? 'checked' : '') + '>' + "Solicitar consentimiento del usuario" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Archivos" + '</b></div>';
            x += '<div><label for=d20flag5><input type=checkbox id=d20flag5 class="form-check-input me-2" ' + ((consent & 0x0004) ? 'checked' : '') + '>' + "Notificar al usuario" + '</label></div>';
            x += '<div><label for=d20flag6><input type=checkbox id=d20flag6 class="form-check-input me-2" ' + ((consent & 0x0020) ? 'checked' : '') + '>' + "Solicitar consentimiento del usuario" + '</label></div>';

            setModalContent('xxAddAgent', title, x);
            xxdialogButtons = 3, xxdialogTag = editType;
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p20editmeshconsentEx(xxdialogButtons, xxdialogTag);
            });

            if (serverinfo.consent) {
                if (serverinfo.consent & 0x0001) { Q('d20flag1').checked = true; }
                if (serverinfo.consent & 0x0008) { Q('d20flag2').checked = true; }
                if (serverinfo.consent & 0x0002) { Q('d20flag3').checked = true; }
                if (serverinfo.consent & 0x0010) { Q('d20flag4').checked = true; }
                if (serverinfo.consent & 0x0004) { Q('d20flag5').checked = true; }
                if (serverinfo.consent & 0x0020) { Q('d20flag6').checked = true; }
                if (serverinfo.consent & 0x0040) { Q('d20flag7').checked = true; }
                QE('d20flag1', !(serverinfo.consent & 0x0001));
                QE('d20flag2', !(serverinfo.consent & 0x0008));
                QE('d20flag3', !(serverinfo.consent & 0x0002));
                QE('d20flag4', !(serverinfo.consent & 0x0010));
                QE('d20flag5', !(serverinfo.consent & 0x0004));
                QE('d20flag6', !(serverinfo.consent & 0x0020));
                QE('d20flag7', !(serverinfo.consent & 0x0040));
            }
        }

        function p20editmeshconsentEx(b, editType) {
            var consent = 0;
            if (Q('d20flag1').checked) { consent += 0x0001; }
            if (Q('d20flag2').checked) { consent += 0x0008; }
            if (Q('d20flag3').checked) { consent += 0x0002; }
            if (Q('d20flag4').checked) { consent += 0x0010; }
            if (Q('d20flag5').checked) { consent += 0x0004; }
            if (Q('d20flag6').checked) { consent += 0x0020; }
            if (Q('d20flag7').checked) { consent += 0x0040; }
            if (editType == 1) { meshserver.send({ action: 'editmesh', meshid: currentMesh._id, consent: consent }); }
            if (editType == 2) { meshserver.send({ action: 'edituser', id: currentUser._id, consent: consent }); }
            if (editType == 3) { meshserver.send({ action: 'changedevice', nodeid: currentNode._id, consent: consent }); }
            if (editType == 4) { meshserver.send({ action: 'editusergroup', ugrpid: currentUserGroup._id, consent: consent }); }
        }

        function p20editmeshfeatures() {
            if (xxdialogMode) return;
            var flags = (currentMesh.flags) ? currentMesh.flags : 0, x = '', expire = 0;
            if ((typeof currentMesh.expireDevs == 'number') && (currentMesh.expireDevs > 0)) { expire = currentMesh.expireDevs; if (expire > 2000) { expire = 2000; } }
            if ((serverinfo.devGroupSessionRecording == 1) && (currentMesh.mtype != 4)) {
                x += '<div class="form-check"><label><input type=checkbox id=d20flag4 class="form-check-input me-2" onchange=p20editmeshfeaturesValidate() ' + ((flags & 4) ? 'checked' : '') + '>' + "Grabar sesiones" + '</label><br></div>';
            }
            if ((currentMesh.mtype == 2) || (currentMesh.mtype == 4)) {
                x += '<div class="form-check"><label><input type=checkbox id=d20flag2 class="form-check-input me-2" onchange=p20editmeshfeaturesValidate() ' + ((flags & 2) ? 'checked' : '') + '>' + ((currentMesh.mtype == 4) ? "Sincronizar el nombre del dispositivo del servidor con el nombre del puerto" : "Sincronizar el nombre del dispositivo del servidor con el nombre del host") + '</label><br></div>';
                if (currentMesh.mtype == 2) {
                    x += '<div style="margin-left:12px"><label><input type=checkbox id=d20flag8 class="form-check-input me-2" onchange=p20editmeshfeaturesValidate() ' + ((flags & 8) ? 'checked' : '') + '>' + "Preferir el valor de --agentName" + '</label><br></div>';
                    x += '<div style="margin-left:12px"><label><input type=checkbox id=d20flag16 class="form-check-input me-2" onchange=p20editmeshfeaturesValidate() ' + ((flags & 16) ? 'checked' : '') + '>' + "Permitir anular el nombre del dispositivo del servidor hasta la próxima conexión" + '</label><br></div>';
                }
                x += '<div class="form-check"><label><input type=checkbox id=d20flag1 class="form-check-input me-2" onchange=p20editmeshfeaturesValidate() ' + ((flags & 1) ? 'checked' : '') + '>' + "Remover el dispositivo al desconectar" + '</label><br></div>';
            }
            x += '<div class="form-check"><label><input type=checkbox id=d20expireDevice class="form-check-input me-2" onchange=p20editmeshfeaturesValidate() ' + ((expire > 0) ? 'checked' : '') + '>' + "Eliminar automáticamente los dispositivos inactivos" + '</label><br></div>';
            x += '<div style=margin-left:20px id=d20expireDeviceDev>' + "Desactivar días hasta la eliminación" + ' <label><input type=number inputmode=numeric class="form-control" onchange=p20editmeshfeaturesValidate() onkeyup=p20editmeshfeaturesValidate() onpaste=p20editmeshfeaturesValidate() onkeydown=p20editmeshfeaturesValidate() maxlength=4 min=1 max=2000 style=width:80px value=\"' + ((expire == 0) ? 30 : expire) + '\" id=d20expireDeviceDays></label><br></div>';

            if (serverinfo.autoremoveinactivedevices) {
                if (serverinfo.autoremoveinactivedevices == 1) {
                    x += format('<span style=font-size:x-small;margin-left:6px>' + "De forma predeterminada, los dispositivos inactivos se eliminarán después de 1 día." + '</span>', serverinfo.autoremoveinactivedevices);
                } else {
                    x += format('<span style=font-size:x-small;margin-left:6px>' + "De forma predeterminada, los dispositivos inactivos se eliminarán después de {0} días." + '</span>', serverinfo.autoremoveinactivedevices);
                }
            }

            setModalContent('xxAddAgent', "Editar Características del Grupo de Dispositivos", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p20editmeshfeaturesEx);
            p20editmeshfeaturesValidate();
        }

        function p20editmeshfeaturesValidate() {
            var flags = 0, ok = true;
            if (((currentMesh.mtype == 2) || (currentMesh.mtype == 4)) && (Q('d20flag1').checked)) { flags += 1; }
            if (currentMesh.mtype == 2) {
                if (Q('d20flag2').checked) {
                    flags += 2;
                    if (event.currentTarget.id == 'd20flag2') { Q('d20flag8').checked = true; Q('d20flag16').checked = true; }
                }
                for (const flag of [8, 16]) {
                    const element = Q('d20flag' + flag);
                    if ((element.checked = element.checked && !(element.disabled = !(flags & 2)))) { flags += flag; }
                }
            }
            QE('d20expireDevice', (flags & 1) == 0);
            var x = ((flags & 1) == 0) && Q('d20expireDevice').checked;
            QV('d20expireDeviceDev', x);
            if (x) { var y = parseInt(Q('d20expireDeviceDays').value); if (isNaN(y) || (y < 1) || (y > 2000) || (y != Q('d20expireDeviceDays').value)) { ok = false; } }
            QE('idx_dlgOkButton', ok);
        }

        function p20editmeshfeaturesEx() {
            var flags = 0;
            if ((currentMesh.mtype == 2) || (currentMesh.mtype == 4)) {
                if (Q('d20flag1').checked) { flags += 1; }
                if (Q('d20flag2').checked) { flags += 2; }
                if (Q('d20flag8').checked) { flags += 8; }
                if (Q('d20flag16').checked) { flags += 16; }
            }
            if ((serverinfo.devGroupSessionRecording == 1) && (currentMesh.mtype != 4)) { if (Q('d20flag4').checked) { flags += 4; } }
            var expireDevs = 0;
            if (((flags & 1) == 0) && Q('d20expireDevice').checked) { expireDevs = parseInt(Q('d20expireDeviceDays').value); }
            meshserver.send({ action: 'editmesh', meshid: currentMesh._id, flags: flags, expireDevs: expireDevs });
        }

        function p20showAddMeshUserDialog(userid, selected) {
            if (xxdialogMode) return false;
            var x = '';
            if ((userid == null) || (userid == 5)) {
                if (selected == null) {
                    if (userid == null) { x += "Permitir a los usuarios administrar este grupo de dispositivos y los dispositivos en este grupo."; } else { x += "Permitir a los usuarios administrar este dispositivo."; }
                    if (features & 0x00080000) { x += " Los usuarios deben iniciar sesión en este servidor una vez antes de poder ser añadidos a un grupo de dispositivos." }
                    x += '<br /><br />';
                }
                x += '<div style=\'position:relative\'>';
                x += addHtmlFormFloating("Identificadores de Usuario", '<input id=dp20username maxlength=256 onchange=p20validateAddMeshUserDialog() onkeyup=p20validateAddMeshUserDialog() placeholder="user1, user2, user3" class="form-control" />');
                x += '<div id=dp20usersuggest class=suggestionBox style=\'top:30px;left:130px;display:none\'></div>';
                x += '</div><br>';
            } else if (userid == 1) {
                var y = '';
                if (selected == null) {
                    var omeshs = getOrderedList(meshes, 'name');
                    for (var i in omeshs) { if ((currentUser.links == null) || (currentUser.links[omeshs[i]._id] == null)) { y += '<option value=' + encodeURIComponentEx(omeshs[i]._id) + '>' + EscapeHtml(omeshs[i].name) + '</option>'; } }
                } else {
                    y += '<option value=' + selected + '>' + EscapeHtml(meshes[decodeURIComponent(selected)].name) + '</option>';
                }
                x += addHtmlFormFloating("Grupo de Dispositivos", '<select onchange=p20validateAddMeshUserDialog() id=dp2groupid class=form-select' + (selected ? ' disabled' : '') + '>' + y + '</select>');
            } else if (userid == 2) {
                if (usergroups == null) return;
                var y = '';
                var ousergroups = getOrderedList(usergroups, 'name');
                for (var i in ousergroups) {
                    if (currentMesh._id.split('/')[1] != ousergroups[i]._id.split('/')[1]) continue;
                    if ((currentMesh.links == null) || (currentMesh.links[ousergroups[i]._id] == null)) { y += '<option value=' + encodeURIComponentEx(ousergroups[i]._id) + '>' + EscapeHtml(ousergroups[i].name) + '</option>'; }
                }
                x += addHtmlFormFloating("Grupo de Usuarios", '<select onchange=p20validateAddMeshUserDialog() id=dp2groupid class="form-select">' + y + '</select>');
            } else if (userid == 6) {
                if (usergroups == null) return;
                var y = '';
                if (selected == null) {
                    var ousergroups = getOrderedList(usergroups, 'name');
                    for (var i in ousergroups) {
                        if ((ousergroups[i].membershipType != null) || (currentNode._id.split('/')[1] != ousergroups[i]._id.split('/')[1])) continue;
                        if ((currentNode.links == null) || (currentNode.links[ousergroups[i]._id] == null)) { y += '<option value=' + encodeURIComponentEx(ousergroups[i]._id) + '>' + EscapeHtml(ousergroups[i].name) + '</option>'; }
                    }
                } else {
                    y += '<option value=' + selected + '>' + EscapeHtml(usergroups[decodeURIComponent(selected)].name) + '</option>';
                }
                x += addHtmlFormFloating("Grupo de Usuarios", '<select onchange=p20validateAddMeshUserDialog() id=dp2groupid ' + (selected ? ' disabled' : '') + ' class="form-select">' + y + '</select>');
            } else if (userid == 3) {
                var y = '';
                if (selected) { selected = decodeURIComponent(selected); }
                var omeshs = getOrderedList(meshes, 'name');
                for (var i in omeshs) { if ((selected != null) || (currentUserGroup.links == null) || (currentUserGroup.links[omeshs[i]._id] == null)) { y += '<option value=' + encodeURIComponentEx(omeshs[i]._id) + ((selected == omeshs[i]._id) ? ' selected' : ' ') + '>' + EscapeHtml(omeshs[i].name) + '</option>'; } }
                x += addHtmlFormFloating("Grupo de Dispositivos", '<select onchange=p20validateAddMeshUserDialog(' + userid + ') id=dp2groupid class="form-select">' + y + '</select>');
            } else if ((userid == 4) || (userid == 7)) {
                var y = '', selectedMeshId = null, selectedNode = null;
                if (selected != null) { selectedNode = getNodeFromId(decodeURIComponent(selected)); if (selectedNode != null) { selectedMeshId = selectedNode.meshid; } }
                var omeshs = getOrderedList(meshes, 'name');
                for (var i in omeshs) {
                    if ((omeshs[i].links[userinfo._id] != null) && (omeshs[i].links[userinfo._id].rights & 7)) { // Only show device groups that we have user administrator for.
                        if (selectedMeshId == null) { selectedMeshId = omeshs[i]._id; } y += '<option value=' + encodeURIComponentEx(omeshs[i]._id) + ((selectedMeshId == omeshs[i]._id) ? ' selected' : ' ') + '>' + EscapeHtml(omeshs[i].name) + '</option>';
                    }
                }
                x += addHtmlFormFloating("Grupo de Dispositivos", '<select onchange=p20changeMeshAddMeshUserDialog(' + userid + ') id=dp2meshid class="form-select">' + y + '</select>');
                y = '';
                var onodes = getOrderedList(nodes, 'name');
                for (var i in onodes) { if (onodes[i].meshid == selectedMeshId) { y += '<option value=' + encodeURIComponentEx(onodes[i]._id) + ((selectedNode == onodes[i]) ? ' selected' : ' ') + '>' + EscapeHtml(onodes[i].name) + '</option>'; } }
                x += addHtmlFormFloating("Dispositivo", '<select onchange=p20validateAddMeshUserDialog(' + userid + ') id=dp2nodeid class="form-select">' + y + '</select>');
            } else {
                userid = decodeURIComponent(userid);
                var uname = userid.split('/')[2];
                if (users && users[userid]) { uname = users[userid].name; }
                if (usergroups && usergroups[userid]) { uname = usergroups[userid].name; }
                if (userinfo._id == userid) { uname = userinfo.name; }
                if (userid.startsWith('ugrp/')) {
                    x += format("Permisos de grupo para {0}.", uname) + '<br /><br />';
                } else {
                    x += format("Permisos de grupo para el usuario {0}.", uname) + '<br /><br />';
                }
            }

            var urights = -1, meshRightsActive = ((userid != 4) && (userid != 5) && (userid != 6) && (userid != 7));

            x += '<div style="height:120px;overflow-y:scroll;border:1px solid var(--sub-menu-color);resize:vertical;padding:8px;border-radius:0.375rem;">';
            if (meshRightsActive) {
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20fulladmin>' + "Administrador Total" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editmesh>' + "Editar Grupo de Dispositivos" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20manageusers>' + "Administrar Usuarios del Grupo de Dispositivos" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20managecomputers>' + "Administrar Computadoras del Grupo de Dispositivos" + '</label><br>';
            }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotecontrol>' + "Control Remoto y Relay" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remoteview style=margin-left:12px>' + "Sólo Vista Remota" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotelimitedinput style=margin-left:12px>' + "Sólo Entrada Limitada" + '</label><br>';
            if (serverinfo.guestdevicesharing !== false) { x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20guestshare style=margin-left:12px>' + "Compartir como invitado" + '</label><br>'; }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nodesktop style=margin-left:12px>' + "Sin Acceso al Escritorio" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noterminal style=margin-left:12px>' + "Sin Acceso a la Terminal" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nofiles style=margin-left:12px>' + "Sin Acceso a Archivos" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noamt style=margin-left:12px>' + "Sin Intel&reg; AMT" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshagentconsole>' + "Consola del Agente Mesh" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshserverfiles>' + "Archivos del Servidor" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20wakedevices>' + "Despertar Dispositivos" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editnotes>' + "Editar Notas del Dispositivo" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20limitevents>' + "Mostrar Solo Eventos Propios" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20chatnotify>' + "Chatear & Notificar" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20uninstall>' + "Desinstalar Agente / Eliminar Dispositivo" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20commands>' + "Comandos Remotos" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20resetoff>' + "Reiniciar / Apagar" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20details>' + "Detalles del dispositivo" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20relay>' + "Usar como Relay" + '</label><br>';
            x += '</div>';

            if (userid == null) {
                xxdialogButtons = 3; xxdialogTag = null;
                setModalContent('xxAddAgent', "Agregar Usuarios al Grupo de Dispositivos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                QE('p20fulladmin', GetMeshRights(currentMesh) == 0xFFFFFFFF);
                Q('dp20username').focus();
            } else if (userid === 1) {
                xxdialogButtons = selected ? 7 : 3; xxdialogTag = userid;
                setModalContent('xxAddAgent', (selected == null) ? "Agregar Permisos de Grupo de Dispositivos" : "Editar Permisos del Grupo de Dispositivos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                if (selected != null) { urights = meshes[decodeURIComponent(selected)].links[currentUser._id].rights; }
                if (urights == 0xFFFFFFFF) { Q('p20fulladmin').checked = true; urights = -1; }
            } else if (userid === 2) {
                xxdialogButtons = 3; xxdialogTag = userid;
                setModalContent('xxAddAgent', "Agregar Grupo de Usuarios", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                QE('p20fulladmin', GetMeshRights(currentMesh) == 0xFFFFFFFF);
            } else if (userid === 3) {
                xxdialogButtons = selected ? 7 : 3; xxdialogTag = userid;
                setModalContent('xxAddAgent', (selected == null) ? "Agregar Grupo de Dispositivos" : "Editar Grupo de Dispositivos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                QE('dp2groupid', selected == null);
                if (selected != null) { urights = currentUserGroup.links[decodeURIComponent(selected)].rights; }
                if (urights == 0xFFFFFFFF) { Q('p20fulladmin').checked = true; urights = -1; }
            } else if (userid === 4) {
                xxdialogButtons = selected ? 7 : 3; xxdialogTag = userid;
                setModalContent('xxAddAgent', (selected == null) ? "Agregar Permisos de Dispositivo" : "Editar Permisos del Dispositivo", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                QE('dp2meshid', selected == null);
                QE('dp2nodeid', selected == null);
            } else if (userid === 7) {
                xxdialogButtons = selected ? 7 : 3; xxdialogTag = userid;
                setModalContent('xxAddAgent', (selected == null) ? "Agregar Permisos de Dispositivo" : "Editar Permisos del Dispositivo", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                QE('dp2meshid', selected == null);
                QE('dp2nodeid', selected == null);
                if (selected != null) { urights = currentUserGroup.links[decodeURIComponent(selected)].rights; QE('dp20username', false); }
            } else if (userid === 5) {
                xxdialogButtons = selected ? 7 : 3; xxdialogTag = userid;
                setModalContent('xxAddAgent', selected ? "Editar Permisos de Dispositivo de Usuario" : "Agregar Permisos del Usuario del Dispositivo", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                if (selected != null) {
                    selected = decodeURIComponent(selected);
                    if ((users != null) && (users[selected] != null)) { Q('dp20username').value = users[selected].name; } else { Q('dp20username').value = selected.split('/')[2]; }
                    urights = currentNode.links[selected].rights;
                    QE('dp20username', false);
                }
                Q('dp20username').focus();
            } else if (userid === 6) {
                xxdialogButtons = selected ? 7 : 3; xxdialogTag = userid;
                setModalContent('xxAddAgent', selected ? "Editar Permisos de Dispositivo de Grupo de Usuarios" : "Agregar Permisos del Grupo de Usuarios del Dispositivo", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                if (selected != null) { urights = currentNode.links[decodeURIComponent(selected)].rights; }
            } else {
                if (userid.startsWith('ugrp/')) {
                    xxdialogButtons = 7, xxdialogTag = userid;
                    setModalContent('xxAddAgent', "Editar Permisos del Grupo de Dispositivos", x);
                } else {
                    xxdialogButtons = 7, xxdialogTag = userid;
                    setModalContent('xxAddAgent', "Editar Permisos de Grupo de Dispositivos de Usuario", x);
                }
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                    p20showAddMeshUserDialogEx(xxdialogButtons, xxdialogTag);
                });
                var cmeshrights = GetMeshRights(currentMesh), urights = GetMeshRights(currentMesh, userid);
                if (urights == 0xFFFFFFFF) { Q('p20fulladmin').checked = true; urights = -1; }
                QE('p20fulladmin', GetMeshRights(currentMesh) == 0xFFFFFFFF);
            }

            if (urights != -1) {
                if (meshRightsActive) {
                    if (urights & 1) { Q('p20editmesh').checked = true; }
                    if (urights & 2) { Q('p20manageusers').checked = true; }
                    if (urights & 4) { Q('p20managecomputers').checked = true; }
                }
                if (urights & 8) {
                    Q('p20remotecontrol').checked = true;
                    if (urights & 65536) { Q('p20nodesktop').checked = true; }
                    if (urights & 256) { Q('p20remoteview').checked = true; }
                    if ((urights & 524288) && (serverinfo.guestdevicesharing !== false)) { Q('p20guestshare').checked = true; }
                    if (urights & 512) { Q('p20noterminal').checked = true; }
                    if (urights & 1024) { Q('p20nofiles').checked = true; }
                    if (urights & 2048) { Q('p20noamt').checked = true; }
                    if (urights & 4096) { Q('p20remotelimitedinput').checked = true; }
                }
                if (urights & 16) { Q('p20meshagentconsole').checked = true; }
                if (urights & 32) { Q('p20meshserverfiles').checked = true; }
                if (urights & 64) { Q('p20wakedevices').checked = true; }
                if (urights & 128) { Q('p20editnotes').checked = true; }
                if (urights & 8192) { Q('p20limitevents').checked = true; }
                if (urights & 16384) { Q('p20chatnotify').checked = true; }
                if (urights & 32768) { Q('p20uninstall').checked = true; }
                if (urights & 131072) { Q('p20commands').checked = true; }
                if (urights & 262144) { Q('p20resetoff').checked = true; }
                if ((urights & 524288) && (serverinfo.guestdevicesharing !== false)) { Q('p20guestshare').checked = true; }
                if (urights & 1048576) { Q('p20details').checked = true; }
                if (urights & 2097152) { Q('p20relay').checked = true; }
            }

            p20validateAddMeshUserDialog(userid);
            return false;
        }

        function p20changeMeshAddMeshUserDialog(userid) {
            var y = '', meshid = decodeURIComponent(Q('dp2meshid').value);
            for (var i in nodes) { if (nodes[i].meshid == meshid) { y += '<option value=' + encodeURIComponentEx(nodes[i]._id) + '>' + EscapeHtml(nodes[i].name) + '</option>'; } }
            QH('dp2nodeid', y);
            p20validateAddMeshUserDialog(userid);
        }

        function p20setname(name) {
            name = decodeURIComponent(name);
            var xusers = Q('dp20username').value.split(',');
            for (var i in xusers) { xusers[i] = xusers[i].trim(); }
            xusers[xusers.length - 1] = name;
            Q('dp20username').value = xusers.join(', ');
            p20validateAddMeshUserDialog();
            return false;
        }

        function p20validateAddMeshUserDialog(updateId) {
            var ok = true;

            if (updateId === 4) {
                // Update user device rights
                var devrights = 0, nodeid = decodeURIComponent(Q('dp2nodeid').value);
                if ((nodeid != '') && (currentUser.links != null) && (currentUser.links[nodeid] != null)) { devrights = currentUser.links[nodeid].rights; }
                Q('p20remotecontrol').checked = ((devrights & 8) != 0);
                Q('p20meshagentconsole').checked = ((devrights & 16) != 0);
                Q('p20meshserverfiles').checked = ((devrights & 32) != 0);
                Q('p20wakedevices').checked = ((devrights & 64) != 0);
                Q('p20editnotes').checked = ((devrights & 128) != 0);
                Q('p20remoteview').checked = ((devrights & 256) != 0);
                Q('p20noterminal').checked = ((devrights & 512) != 0);
                Q('p20nofiles').checked = ((devrights & 1024) != 0);
                Q('p20noamt').checked = ((devrights & 2048) != 0);
                Q('p20remotelimitedinput').checked = ((devrights & 4096) != 0);
                Q('p20limitevents').checked = ((devrights & 8192) != 0);
                Q('p20chatnotify').checked = ((devrights & 16384) != 0);
                Q('p20uninstall').checked = ((devrights & 32768) != 0);
                Q('p20nodesktop').checked = ((devrights & 65536) != 0);
                Q('p20commands').checked = ((devrights & 131072) != 0);
                Q('p20resetoff').checked = ((devrights & 262144) != 0);
                if (serverinfo.guestdevicesharing !== false) { Q('p20guestshare').checked = ((devrights & 524288) != 0); }
                Q('p20details').checked = ((devrights & 1048576) != 0);
                Q('p20relay').checked = ((devrights & 2097152) != 0);
                ok = (nodeid != '');
            }

            /*
            var meshrights = null;
            if ((xxdialogTag === 1) || (xxdialogTag === 3)) {
                meshrights = meshes[decodeURIComponent(Q('dp2groupid').value)].links[userinfo._id].rights;
                //meshrights = GetMeshRights(decodeURIComponent(Q('dp2groupid').value));
            } else {
                meshrights = currentMesh.links[userinfo._id].rights;
                //meshrights = GetMeshRights(currentMesh);
            }
            */
            if (Q('dp20username')) {
                var xusers = Q('dp20username').value.split(',');
                for (var i in xusers) {
                    var xuser = xusers[i] = xusers[i].trim();
                    if (xuser.length == 0) { ok = false; } else if (xuser.indexOf('"') >= 0) { ok = false; }
                }

                // Fill the suggestion box
                var showsuggestbox = false, exactMatch = false;
                if (users != null) {
                    var lastuser = xusers[xusers.length - 1].trim(), lastuserl = lastuser.toLowerCase(), matchingUsers = [];
                    if (lastuser.length > 0) {
                        for (var i in users) {
                            var userSplit = users[i]._id.split('/');
                            if ((currentMesh != null) && (currentMesh.domain != userSplit[1])) continue;
                            if ((currentNode != null) && (currentNode.domain != userSplit[1])) continue;
                            if (userSplit[2] === lastuserl) { exactMatch = true; break; }
                            if (users[i].name.toLowerCase().indexOf(lastuserl) >= 0) { matchingUsers.push([users[i]._id, users[i].name]); if (matchingUsers.length >= 8) break; }
                        }
                        if ((exactMatch == false) && (matchingUsers.length > 0)) {
                            var x = '';
                            for (var i in matchingUsers) {
                                var sid = matchingUsers[i][0], sname = matchingUsers[i][1];
                                if (sid.split('/')[2] == sname.toLowerCase()) {
                                    x += '<div class=suggestionBoxItem onclick=\'p20setname("' + encodeURIComponentEx(sid.split('/')[2]) + '")\'>' + EscapeHtml(sname) + '</div>';
                                } else {
                                    x += '<div class=suggestionBoxItem onclick=\'p20setname("' + encodeURIComponentEx(sid.split('/')[2]) + '")\'><div>' + EscapeHtml(sname) + '</div><div class=suggestionBoxSubItem>' + EscapeHtml(sid.split('/')[2]) + '</div></div>';
                                }
                            }
                            QH('dp20usersuggest', x);
                            showsuggestbox = true;
                        }
                    }
                }
                QV('dp20usersuggest', showsuggestbox);
            }
            QE('idx_dlgOkButton', ok);

            var nc;
            if (Q('p20fulladmin') != null) {
                nc = !Q('p20fulladmin').checked;
                //QE('p20fulladmin', meshrights == 0xFFFFFFFF);
                //QE('p20editmesh', nc && (meshrights == 0xFFFFFFFF));
                //QE('p20fulladmin', nc);
                QE('p20editmesh', nc);
                QE('p20manageusers', nc);
                QE('p20managecomputers', nc);
            } else {
                nc = (nodeid != '');
            }
            QE('p20remotecontrol', nc);
            QE('p20meshagentconsole', nc);
            QE('p20meshserverfiles', nc);
            QE('p20wakedevices', nc);
            QE('p20editnotes', nc);
            QE('p20limitevents', nc);
            QE('p20remoteview', nc && Q('p20remotecontrol').checked);
            if (serverinfo.guestdevicesharing !== false) { QE('p20guestshare', nc && Q('p20remotecontrol').checked && (Q('p20remoteview').checked || !Q('p20remotelimitedinput').checked)); }
            QE('p20remotelimitedinput', nc && Q('p20remotecontrol').checked && !Q('p20remoteview').checked);
            QE('p20nodesktop', nc && Q('p20remotecontrol').checked);
            QE('p20noterminal', nc && Q('p20remotecontrol').checked);
            QE('p20nofiles', nc && Q('p20remotecontrol').checked);
            QE('p20noamt', nc && Q('p20remotecontrol').checked);
            QE('p20chatnotify', nc);
            QE('p20uninstall', nc);
            QE('p20commands', nc);
            QE('p20resetoff', nc);
            QE('p20details', nc);
            QE('p20relay', nc);
        }

        function p20showAddMeshUserDialogEx(b, t) {
            // Get the currently selected rights
            var meshadmin = 0;
            if ((Q('p20fulladmin') != null) && (Q('p20fulladmin').checked == true)) { meshadmin = 0xFFFFFFFF; } else {
                if (Q('p20fulladmin') != null) {
                    if (Q('p20editmesh').checked == true) meshadmin += 1;
                    if (Q('p20manageusers').checked == true) meshadmin += 2;
                    if (Q('p20managecomputers').checked == true) meshadmin += 4;
                }
                if (Q('p20remotecontrol').checked == true) meshadmin += 8;
                if (Q('p20meshagentconsole').checked == true) meshadmin += 16;
                if (Q('p20meshserverfiles').checked == true) meshadmin += 32;
                if (Q('p20wakedevices').checked == true) meshadmin += 64;
                if (Q('p20editnotes').checked == true) meshadmin += 128;
                if (Q('p20remoteview').checked == true) meshadmin += 256;
                if (Q('p20nodesktop').checked == true) meshadmin += 65536;
                if (Q('p20noterminal').checked == true) meshadmin += 512;
                if (Q('p20nofiles').checked == true) meshadmin += 1024;
                if (Q('p20noamt').checked == true) meshadmin += 2048;
                if ((Q('p20remotelimitedinput').checked == true) && (!Q('p20remoteview').checked)) meshadmin += 4096;
                if (Q('p20limitevents').checked == true) meshadmin += 8192;
                if (Q('p20chatnotify').checked == true) meshadmin += 16384;
                if (Q('p20uninstall').checked == true) meshadmin += 32768;
                if (Q('p20commands').checked == true) meshadmin += 131072;
                if (Q('p20resetoff').checked == true) meshadmin += 262144;
                if ((serverinfo.guestdevicesharing !== false) && (Q('p20guestshare').checked == true) && (Q('p20remoteview').checked || (!Q('p20remoteview').checked && !Q('p20remotelimitedinput').checked))) meshadmin += 524288;
                if (Q('p20details').checked == true) meshadmin += 1048576;
                if (Q('p20relay').checked == true) meshadmin += 2097152;
            }

            // Clean up incorrect rights. If Remote Control is not selected, remove flags that don't make sense.
            if ((meshadmin & 8) == 0) {
                // Remove 256, 512, 1024, 2048, 4096, 65536
                if (meshadmin & 256) { meshadmin -= 256; }
                if (meshadmin & 512) { meshadmin -= 512; }
                if (meshadmin & 1024) { meshadmin -= 1024; }
                if (meshadmin & 2048) { meshadmin -= 2048; }
                if (meshadmin & 4096) { meshadmin -= 4096; }
                if (meshadmin & 65536) { meshadmin -= 65536; }
            }

            // Send the action to the server
            if (t === 1) {
                // Add current user to device group
                var meshid = decodeURIComponent(Q('dp2groupid').value), mesh = meshes[meshid];
                if (mesh != null) { meshserver.send({ action: 'addmeshuser', meshid: meshid, meshname: mesh.name, userids: [currentUser._id], meshadmin: meshadmin, remove: (b == 2) }); }
            } else if (t === 2) {
                // Add user group to device group
                var ugrpid = decodeURIComponent(Q('dp2groupid').value), mesh = meshes[currentMesh._id];
                if (mesh != null) { meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userid: ugrpid, meshadmin: meshadmin, remove: (b == 2) }); }
            } else if (t === 3) {
                // Add device group to current user group
                var meshid = decodeURIComponent(Q('dp2groupid').value), mesh = meshes[meshid];
                if (mesh != null) { meshserver.send({ action: 'addmeshuser', meshid: meshid, meshname: mesh.name, userids: [currentUserGroup._id], meshadmin: meshadmin, remove: (b == 2) }); }
            } else if (t === 4) {
                // Add current user to device
                var nodeid = decodeURIComponent(Q('dp2nodeid').value), node = getNodeFromId(nodeid);
                if (node != null) { meshserver.send({ action: 'adddeviceuser', nodeid: nodeid, nodename: node.name, userids: [currentUser._id], rights: meshadmin, remove: (b == 2) }); }
            } else if (t === 5) {
                // Add users to device
                var users = Q('dp20username').value.split(','), users2 = [];
                for (var i in users) { users2.push(users[i].trim()); }
                meshserver.send({ action: 'adddeviceuser', nodeid: currentNode._id, nodename: currentNode.name, usernames: users2, rights: meshadmin, remove: (b == 2) });
            } else if (t === 6) {
                // Add user group to device
                var ugrpid = decodeURIComponent(Q('dp2groupid').value);
                if (currentNode != null) { meshserver.send({ action: 'adddeviceuser', nodeid: currentNode._id, nodename: currentNode.name, userids: [ugrpid], rights: meshadmin, remove: (b == 2) }); }
            } else if (t === 7) {
                // Add current user group to device
                var nodeid = decodeURIComponent(Q('dp2nodeid').value), node = getNodeFromId(nodeid);
                if (node != null) { meshserver.send({ action: 'adddeviceuser', nodeid: nodeid, nodename: node.name, userids: [currentUserGroup._id], rights: meshadmin, remove: (b == 2) }); }
            } else {
                // Add user to device group
                if (t == null) {
                    var users = Q('dp20username').value.split(','), users2 = [];
                    for (var i in users) { users2.push(users[i].trim()); }
                    meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, usernames: users2, meshadmin: meshadmin, remove: (b == 2) });
                } else {
                    meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userids: [t], meshadmin: meshadmin, remove: (b == 2) });
                }
            }
        }

        function p20viewuser(userid) {
            if (xxdialogMode) return;
            var xuserid = decodeURIComponent(userid);
            var cmeshrights = GetMeshRights(currentMesh), meshrights = GetMeshRights(currentMesh, xuserid);
            if (((userinfo._id) != xuserid) && (cmeshrights == 0xFFFFFFFF || (((cmeshrights & 2) != 0) && (meshrights != 0xFFFFFFFF)))) {
                p20showAddMeshUserDialog(userid);
            } else {
                var r = [];
                if (meshrights == 0xFFFFFFFF) r.push("Administrador Total (todos los permisos)"); else {
                    if ((meshrights & 1) != 0) r.push("Editar Grupo de Dispositivos");
                    if ((meshrights & 2) != 0) r.push("Administrar Usuarios del Grupo de Dispositivos");
                    if ((meshrights & 4) != 0) r.push("Administrar Computadoras del Grupo de Dispositivos");
                    if ((meshrights & 8) != 0) r.push("Control Remoto");
                    if ((meshrights & 16) != 0) r.push("Consola de Agente");
                    if ((meshrights & 32) != 0) r.push("Archivos del Servidor");
                    if ((meshrights & 64) != 0) r.push("Despertar Dispositivos");
                    if ((meshrights & 128) != 0) r.push("Editar Notas");
                    if (((meshrights & 8) != 0) && (meshrights & 256) != 0) r.push("Sólo Vista Remota");
                    if (((meshrights & 8) != 0) && (meshrights & 65536) != 0) r.push("Sin Escritorio");
                    if (((meshrights & 8) != 0) && (meshrights & 512) != 0) r.push("Sin Terminal");
                    if (((meshrights & 8) != 0) && (meshrights & 1024) != 0) r.push("Sin Archivos");
                    if (((meshrights & 8) != 0) && (meshrights & 2048) != 0) r.push("Sin Intel&reg; AMT");
                    if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("Entrada Limitada");
                    if ((meshrights & 8192) != 0) r.push("Sólo Eventos Propios");
                    if ((meshrights & 16384) != 0) r.push("Chatear & Notificar");
                    if ((meshrights & 32768) != 0) r.push("Desinstalar");
                    if ((meshrights & 131072) != 0) r.push("Comandos");
                    if ((meshrights & 262144) != 0) r.push("Reset/Apagado");
                    if ((meshrights & 524288) != 0) r.push("Intercambio");
                    if ((meshrights & 1048576) != 0) r.push("Detalles");
                    if ((meshrights & 2097152) != 0) r.push("Relé");
                }
                if (r.length == 0) { r.push("Sin Permisos"); }
                var uname = xuserid.split('/')[2];
                if (users && users[xuserid]) { uname = users[xuserid].name; }
                if (userinfo._id == xuserid) { uname = userinfo.name; }
                var buttons = 1, x = addHtmlValue("Nombre de Usuario", EscapeHtml(decodeURIComponent(uname)));
                if (xuserid.split('/')[2] != uname) { x += addHtmlValue("Identificador de Usuario", EscapeHtml(xuserid.split('/')[2])); }

                x += addHtmlValue("Permisos", r.join(", "));
                if (((userinfo._id) != xuserid) && (cmeshrights == 0xFFFFFFFF || (((cmeshrights & 2) != 0) && (meshrights != 0xFFFFFFFF)))) buttons += 4;
                setModalContent('xxAddAgent', "Usuario de Grupo de Dispositivos", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p20viewuserEx(buttons, xuserid));
            }
        }

        function p20viewuserEx(button, userid) {
            if (button != 2) return;
            var uname = userid.split('/')[2];
            if (users && users[userid]) { uname = users[userid].name; }
            if (usergroups && usergroups[userid]) { uname = usergroups[userid].name; }
            if (userinfo._id == userid) { uname = userinfo.name; }
            if (userid.startsWith('user/')) {
                setModalContent('xxAddAgent', "Remover Permisos de Usuario", format("¿Confirmar la eliminación de los permisos del usuario \"{0}\"?", EscapeHtml(decodeURIComponent(uname))));
            }
            if (userid.startsWith('ugrp/')) {
                setModalContent('xxAddAgent', "Remover Permisos de Grupo de Usuarios", format("¿Confirmar la eliminación de los permisos para el grupo de usuarios \"{0}\"?", EscapeHtml(decodeURIComponent(uname))), userid);
            }
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p20viewuserEx2(3, userid));
        }
        function p20deleteUser(e, userid) { haltEvent(e); p20viewuserEx(2, decodeURIComponent(userid)); return false; }
        function p20viewuserEx2(button, userid) { meshserver.send({ action: 'removemeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userid: userid }); }

        function p20editmeshInviteCode() {
            if (xxdialogMode) return false;
            var meshrights = GetMeshRights(currentMesh);

            var servername = serverinfo.name;
            if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
            var url;
            if (serverinfo.https == true) {
                var portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port);
                url = 'https://' + servername + portStr + domainUrl + 'invite' + ((urlargs.key) ? ('?key=' + urlargs.key) : '');
            } else {
                var portStr = (serverinfo.port == 80) ? '' : (':' + serverinfo.port);
                url = 'http://' + servername + portStr + domainUrl + 'invite' + ((urlargs.key) ? ('?key=' + urlargs.key) : '');
            }

            if (meshrights & 1) {
                // We can edit the mesh invite codes
                var x = "Cuando está habilitado, cualquiera puede usar códigos de invitación para unir dispositivos a este grupo de dispositivos mediante el siguiente enlace público:" + '<br /><br />';
                x += '<div style=width:100%;text-align:center><a rel="noreferrer noopener" target=_blank href="' + url + '">' + url + '</a></div><br />';
                x += '<div style=margin-bottom:5px><label><input id=agentJoinCheck type=checkbox class="form-check-input me-2" onclick=p20editmeshInviteCodeValidate() />' + "Habilitar Códigos de Invitación" + '</label></div>';
                x += addHtmlFormFloating("Códigos de Invitación", '<input id=agentInviteCode class="form-control" onkeyup=p20editmeshInviteCodeValidate() placeholder="code1, code2, code3" />');
                x += addHtmlFormFloating("Agentes", '<select id=agentType class="form-select" onchange=p20editmeshInviteCodeValidate()><option value=0>' + "Todos los agentes disponibles" + '</option><option value=1>' + "Agente de malla de Windows" + '</option><option value=2>' + "Agente de malla de Linux" + '</option><option value=4>' + "Agente de malla de macOS" + '</option><option value=8>' + "Asistente MeshCentral" + '</option></select>');
                x += '<div id=d2agentInstallTypeDiv>';
                x += addHtmlFormFloating("Tipo de Instalación", '<select id=agentInviteType class="form-select"><option value=0>' + "Segundo plano e interactivo" + '</option><option value=2>' + "Segundo plano solamente" + '</option><option value=1>' + "Sólo Interactivo" + '</option></select>');
                x += '</div>';
                setModalContent('xxAddAgent', "Códigos de Invitación", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', p20editmeshInviteCodeEx);
                if (currentMesh.invite != null) {
                    Q('agentJoinCheck').checked = true;
                    Q('agentInviteCode').value = currentMesh.invite.codes.join(', ');
                    if (currentMesh.invite.ag) { Q('agentType').value = currentMesh.invite.ag; }
                    Q('agentInviteType').value = (currentMesh.invite.flags & 3);
                }
                p20editmeshInviteCodeValidate();
            } else {
                // View codes only
                var x = "Los códigos de invitación pueden ser usados por cualquiera para unir dispositivos a este grupo de dispositivos usando el siguiente enlace público:" + '<br /><br />';
                x += '<div style=width:100%;text-align:center><a rel="noreferrer noopener" target=_blank href="' + url + '">' + url + '</a></div><br />';
                x += addHtmlValue("Códigos de Invitación", currentMesh.invite.codes.join(', '));
                x += addHtmlValue("Tipo de Instalación", ["Segundo plano e interactivo", "Sólo Interactivo", "Segundo plano solamente"][currentMesh.invite.flags & 3]);
                setModalContent('xxAddAgent', "Códigos de Invitación", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
        }

        function p20editmeshInviteCodeValidate() {
            var ok = true, codes = Q('agentInviteCode').value.split(',');
            for (var i in codes) { codes[i] = codes[i].trim(); if (codes[i] == '') { ok = false; } }
            QE('agentInviteCode', Q('agentJoinCheck').checked);
            QE('agentType', Q('agentJoinCheck').checked);
            QE('agentInviteType', Q('agentJoinCheck').checked);
            QE('idx_dlgOkButton', (Q('agentJoinCheck').checked == false) || (ok));
            QV('d2agentInstallTypeDiv', parseInt(Q('agentType').value) < 2);
        }

        function p20editmeshInviteCodeEx() {
            if (Q('agentJoinCheck').checked == true) {
                var codes = Q('agentInviteCode').value.split(',');
                for (var i in codes) { codes[i] = codes[i].trim(); }
                meshserver.send({ action: 'editmesh', meshid: currentMesh._id, invite: { codes: codes, flags: parseInt(Q('agentInviteType').value), ag: parseInt(Q('agentType').value) } });
            } else {
                meshserver.send({ action: 'editmesh', meshid: currentMesh._id, invite: '*' });
            }
        }

        function p20editMeshNotify() {
            if (xxdialogMode) return false;
            var meshNotify = 0;
            var emailNotify = ((features2 & 0x00004000) && (userinfo.emailVerified));
            if (userinfo.links && userinfo.links[currentMesh._id] && userinfo.links[currentMesh._id].notify) { meshNotify |= userinfo.links[currentMesh._id].notify; }
            if (userinfo.notify && userinfo.notify[currentMesh._id]) { meshNotify |= userinfo.notify[currentMesh._id]; }
            //var x = "Notification settings must also be turned on in account settings." + '<br /><br />';
            var x = '<div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificaciones de página web" + '</div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Conexiones de dispositivo" + '</label></div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconexiones de dispositivo" + '</label></div>';
            if (currentMesh.mtype < 3) {
                x += '<div class="form-check"><label><input id=p20notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Eventos de escritorio y serial de Intel&reg; AMT" + '</label></div>';
            }
            if (emailNotify) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificaciones por correo electrónico" + '</div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Conexiones de dispositivo" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconexiones de dispositivo" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceHelp class="form-check-input me-2" type=checkbox />' + "Solicitudes de ayuda" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificaciones de Mensajería" + '</div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceConnect class="form-check-input me-2" type=checkbox />' + "Conexiones de dispositivo" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconexiones de dispositivo" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceHelp class="form-check-input me-2" type=checkbox />' + "Solicitudes de ayuda" + '</label></div>';
            }

            setModalContent('xxAddAgent', "Opciones de Notificación", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p20editMeshNotifyEx);

            Q('p20notifyIntelDeviceConnect').checked = (meshNotify & 2);
            Q('p20notifyIntelDeviceDisconnect').checked = (meshNotify & 4);
            if (currentMesh.mtype < 3) { Q('p20notifyIntelAmtKvmActions').checked = (meshNotify & 8); }
            if (emailNotify) {
                Q('p20enotifyIntelDeviceConnect').checked = (meshNotify & 16);
                Q('p20enotifyIntelDeviceDisconnect').checked = (meshNotify & 32);
                Q('p20enotifyIntelDeviceHelp').checked = (meshNotify & 64);
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                Q('p20emsgDeviceConnect').checked = (meshNotify & 128);
                Q('p20emsgDeviceDisconnect').checked = (meshNotify & 256);
                Q('p20emsgDeviceHelp').checked = (meshNotify & 512);
            }
            return false;
        }

        function p20editMeshNotifyEx() {
            var meshNotify = 0;
            var emailNotify = ((features2 & 0x00004000) && (userinfo.emailVerified));
            meshNotify += Q('p20notifyIntelDeviceConnect').checked ? 2 : 0;
            meshNotify += Q('p20notifyIntelDeviceDisconnect').checked ? 4 : 0;
            if (currentMesh.mtype < 3) { meshNotify += Q('p20notifyIntelAmtKvmActions').checked ? 8 : 0; }
            if (emailNotify) {
                meshNotify += Q('p20enotifyIntelDeviceConnect').checked ? 16 : 0;
                meshNotify += Q('p20enotifyIntelDeviceDisconnect').checked ? 32 : 0;
                meshNotify += Q('p20enotifyIntelDeviceHelp').checked ? 64 : 0;
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                meshNotify += Q('p20emsgDeviceConnect').checked ? 128 : 0;
                meshNotify += Q('p20emsgDeviceDisconnect').checked ? 256 : 0;
                meshNotify += Q('p20emsgDeviceHelp').checked ? 512 : 0;
            }
            meshserver.send({ action: 'changemeshnotify', meshid: currentMesh._id, notify: meshNotify });
        }

        //
        // Mesh Summary
        //

        function setupMeshSummaryStats() {
            window.meshPowerChart = new Chart(document.getElementById('meshPowerChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#20F', '#40D', '#60B', '#809', '#A07', '#C05'] }] },
                options: { shadowColor: 'blue', responsive: true, plugins: { legend: { display: false } }, animation: { animateScale: true, animateRotate: true }, layout: { padding: { left: 10, right: 10, top: 10, bottom: 10 } } }
            });
            window.meshOsChart = new Chart(document.getElementById('meshOsChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#20F', '#30E', '#40D', '#50C', '#60B', '#70A', '#809', '#908', '#A07', '#B06', '#C05'] }] },
                options: { responsive: true, plugins: { legend: { display: false } }, animation: { animateScale: true, animateRotate: true }, layout: { padding: { left: 10, right: 10, top: 10, bottom: 10 } } }
            });
            window.meshConnChart = new Chart(document.getElementById('meshConnChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#20F', '#40D', '#60B', '#809', '#A07', '#C05'] }], labels: ["No Conectado", "Agente", "Intel&reg; AMT", "Agente + Intel&reg; AMT"] },
                options: { responsive: true, plugins: { legend: { display: false } }, animation: { animateScale: true, animateRotate: true }, layout: { padding: { left: 10, right: 10, top: 10, bottom: 10 } } }
            });
            window.meshSecurityChart = new Chart(document.getElementById('meshSecurityChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#20F', '#40D', '#60B', '#809', '#A07', '#C05'] }], labels: ["OK", "Antivirus inactivo", "Sin actualización automática", "Cortafuegos no activo", "Múltiples Problemas"] },
                options: { responsive: true, plugins: { legend: { display: false } }, animation: { animateScale: true, animateRotate: true }, layout: { padding: { left: 10, right: 10, top: 10, bottom: 10 } } }
            });
        }

        function p21updateMesh() {
            if (currentMesh == null) return;

            // Add device group name
            var meshrights = GetMeshRights(currentMesh), mname = EscapeHtml(currentMesh.name);
            if (mname.length == 0) { mname = '<i>' + "Ninguno" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span tabindex=0 title="' + "Haz clic aquí para editar el nombre del grupo de dispositivos" + '" onclick=p20editmesh(1) onkeyup="if (event.key == \'Enter\') p20editmesh(1)" role=button>' + mname + ' <i class="fa-solid fa-pencil fa-2xs"></i></span>'; }
            QH('p21meshName', mname);

            // Update charts
            var power = {};
            var conn = {};
            var agentTypes = {};
            var powerStates = {};
            var connectivityStates = [0, 0, 0, 0]; // None, Agent, AMT, Agent + AMT
            var securityStates = [0, 0, 0, 0, 0]; // OK, no AV, no firewall, no update, many Issues
            var showAgents = false;
            var showPower = false;
            var showConn = false;
            var showSecurity = false;
            for (var i in nodes) {
                if (nodes[i].meshid == currentMesh._id) {
                    if (nodes[i].agent) { showAgents = true; if (agentTypes[nodes[i].agent.id] == null) { agentTypes[nodes[i].agent.id] = 1; } else { agentTypes[nodes[i].agent.id]++; } }
                    if (nodes[i].pwr) { showPower = true; if (powerStates[nodes[i].pwr] == null) { powerStates[nodes[i].pwr] = 1; } else { powerStates[nodes[i].pwr]++; } }
                    if (nodes[i].conn == 0) { showConn = true; connectivityStates[0]++; }
                    else if ((nodes[i].conn & 6) != 0) { showConn = true; if ((nodes[i].conn & 1) != 0) { connectivityStates[3]++; } else { connectivityStates[2]++; } }
                    else if ((nodes[i].conn & 1) != 0) { showConn = true; connectivityStates[1]++; }
                    if (nodes[i].wsc) {
                        showSecurity = true;
                        var sok = 0;
                        if (nodes[i].wsc.antiVirus == 'OK') { sok++; }
                        if (nodes[i].wsc.autoUpdate == 'OK') { sok++; }
                        if (nodes[i].wsc.firewall == 'OK') { sok++; }
                        if (sok == 3) { securityStates[0]++; }
                        else if (sok < 2) { securityStates[4]++; }
                        else if (nodes[i].wsc.antiVirus != 'OK') { securityStates[1]++; }
                        else if (nodes[i].wsc.autoUpdate != 'OK') { securityStates[2]++; }
                        else if (nodes[i].wsc.firewall != 'OK') { securityStates[3]++; }
                    }
                }
            }

            var agentsData = [], agentsLabels = [], powerData = [], powerLabels = [], xagentsStr = (currentMesh.mtype == 3) ? agentsStrNoAgent : agentsStr;
            for (var i in agentTypes) { agentsData.push(agentTypes[i]); agentsLabels.push(xagentsStr[i]); }
            for (var i in powerStates) { powerData.push(powerStates[i]); powerLabels.push(powerStatetable[i]); }
            window.meshPowerChart.config.data.datasets[0].data = powerData;
            window.meshPowerChart.config.data.labels = powerLabels;
            window.meshPowerChart.update();
            if (currentMesh.mtype >= 2) {
                window.meshOsChart.config.data.datasets[0].data = agentsData;
                window.meshOsChart.config.data.labels = agentsLabels;
                window.meshOsChart.update();
            }
            window.meshConnChart.config.data.datasets[0].data = connectivityStates;
            window.meshConnChart.update();
            window.meshSecurityChart.config.data.datasets[0].data = securityStates;
            window.meshSecurityChart.update();

            // Update tables
            var x = '', count = 0;
            if (powerData.length > 0) {
                var xpowerStates = [];
                for (var i in powerStates) { xpowerStates.push([powerStatetable[i], powerStates[i]]); }
                xpowerStates.sort(function (a, b) { return -(a[1] - b[1]) });
                x += '<table class="table table-hover table-striped" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col colspan=2 style=text-align:left;width:430px>' + "Estados de Encendido" + '</th></tr>';
                for (var i in xpowerStates) { x += '<tr style=' + (((++count % 2) == 0) ? 'background-color:#DDD' : '') + '><td style=text-align:right;width:60px>' + xpowerStates[i][1] + ' <td> ' + xpowerStates[i][0] + '</tr>'; }
                x += '</tbody></table>';
            }

            if ((agentsData.length > 0) && (currentMesh.mtype >= 2)) {
                count = 0;
                var xagentTypes = [];
                for (var i in agentTypes) { xagentTypes.push([xagentsStr[i], agentTypes[i]]); }
                xagentTypes.sort(function (a, b) { return -(a[1] - b[1]) });
                x += '<table class="table table-hover table-striped" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col colspan=2 style=text-align:left;width:430px>' + "Tipos de Agente" + '</th></tr>';
                for (var i in xagentTypes) { x += '<tr style=' + (((++count % 2) == 0) ? 'background-color:#DDD' : '') + '><td style=text-align:right;width:60px>' + xagentTypes[i][1] + '<td> ' + xagentTypes[i][0] + '</tr>'; }
                x += '</tbody></table>';
            }

            if (showConn) {
                count = 0;
                var xconnectivityStates = [];
                for (var i = 0; i < 4; i++) { xconnectivityStates.push([["No Conectado", "Agente", "Intel&reg; AMT", "Agente + Intel&reg; AMT"][i], connectivityStates[i]]); }
                xconnectivityStates.sort(function (a, b) { return -(a[1] - b[1]) });
                x += '<table class="table table-hover table-striped" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col colspan=2 style=text-align:left;width:430px>' + "Conectividad" + '</th></tr>';
                for (var i = 0; i < 4; i++) { if (xconnectivityStates[i][1] > 0) { x += '<tr style=' + (((++count % 2) == 0) ? 'background-color:#DDD' : '') + '><td style=text-align:right;width:60px>' + xconnectivityStates[i][1] + '<td> ' + xconnectivityStates[i][0] + '</tr>'; } }
                x += '</tbody></table>';
            }

            if (showSecurity) {
                count = 0;
                var xsecurityStates = [];
                for (var i = 0; i < 4; i++) {
                    xsecurityStates.push([[
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:ok")\' width=10 height=10 /> ' + "OK",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noav")\' width=10 height=10 /> ' + "Antivirus inactivo",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noupdate")\' width=10 height=10 /> ' + "Sin actualización automática",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:nofirewall")\' width=10 height=10 /> ' + "Cortafuegos no activo",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:any")\' width=10 height=10 /> ' + "Múltiples Problemas"
                    ][i], securityStates[i]]);
                }
                xsecurityStates.sort(function (a, b) { return -(a[1] - b[1]) });
                x += '<table class="table table-hover table-striped" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col colspan=2 style=text-align:left;width:430px>' + "Seguridad" + '</th></tr>';
                for (var i = 0; i < 4; i++) { if (xsecurityStates[i][1] > 0) { x += '<tr style=' + (((++count % 2) == 0) ? 'background-color:#DDD' : '') + '><td style=text-align:right;width:60px>' + xsecurityStates[i][1] + '<td> ' + xsecurityStates[i][0] + '</tr>'; } }
                x += '</tbody></table>';
            }

            if (x == '') { x = '<i>' + "No hay dispositivos en este grupo de dispositivos." + '</i>'; }

            QH('p21info', x);

            // Only show the OS chart if the mesh is agent type.
            var graphsCount = ((showPower) ? 1 : 0) + (((currentMesh.mtype >= 2) && showAgents) ? 1 : 0) + ((showConn) ? 1 : 0) + ((showSecurity) ? 1 : 0);
            QS('meshPowerChartDiv')['display'] = (showPower) ? 'inline-block' : 'none';
            QS('meshOsChartDiv')['display'] = ((currentMesh.mtype >= 2) && showAgents) ? 'inline-block' : 'none';
            QS('meshConnChartDiv')['display'] = (showConn) ? 'inline-block' : 'none';
            QS('meshSecurityChartDiv')['display'] = (showSecurity) ? 'inline-block' : 'none';
            QS('meshPowerChartDiv')['width'] = (graphsCount > 3) ? '23%' : '31%';
            QS('meshOsChartDiv')['width'] = (graphsCount > 3) ? '23%' : '31%';
            QS('meshConnChartDiv')['width'] = (graphsCount > 3) ? '23%' : '31%';
            QS('meshSecurityChartDiv')['width'] = (graphsCount > 3) ? '23%' : '31%';
        }

        function p21setDevicesFilter(filter) { go(1); Q('KvmSearchInput').value = Q('SearchInput').value = filter; mainUpdate(5); }

        //
        // MY FILES
        //

        var filetreelinkpath;
        var filetreelocation = [];

        function updateFiles() {
            QV('MainMenuMyFiles', ((features & 8) == 0));
            if ((features & 8) != 0) return; // If running on a server without files, exit now.
            var html1 = '', html2 = '', displayPath = '<a href=# style=cursor:pointer onclick="return p5folderup(0)">' + "Raíz" + '</a>', fullPath = 'Root', publicPath, filetreex = filetree, folderdepth = 1;

            // Navigate to path location, build the paths at the same time
            var filetreelocation2 = [], oldlinkpath = filetreelinkpath, checkedBoxes = [], checkboxes = document.getElementsByName('fc');
            for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { checkedBoxes.push(checkboxes[i].value) }; } // Save all existing checked boxes

            filetreelinkpath = '';
            for (var i in filetreelocation) {
                if ((filetreex.f != null) && (filetreex.f[filetreelocation[i]] != null)) {
                    filetreelocation2.push(filetreelocation[i]);
                    fullPath += ' / ' + filetreelocation[i];
                    if ((folderdepth == 1)) {
                        var sp = filetreelocation[i].split('/');
                        publicPath = window.location.origin + domainUrl + sp[0] + 'files/' + sp[2];
                        //if (filetreelocation[i] === userinfo._id) { filetreelinkpath += 'self'; } else { filetreelinkpath += (sp[0] + '/' + sp[2]); }
                        filetreelinkpath += filetreelocation[i];
                    } else {
                        if (filetreelinkpath != '') { filetreelinkpath += '/' + filetreelocation[i]; if (folderdepth > 2) { publicPath += '/' + filetreelocation[i]; } }
                    }
                    filetreex = filetreex.f[filetreelocation[i]];
                    displayPath += ' / <a href=# style=cursor:pointer onclick="return p5folderup(' + folderdepth + ')">' + EscapeHtml(filetreex.n != null ? filetreex.n : filetreelocation[i]) + '</a>';
                    folderdepth++;
                } else {
                    break;
                }
            }
            filetreelocation = filetreelocation2; // In case we could not go down the full path, we set the new path location here.
            var publicfolder = fullPath.toLowerCase().startsWith('root / ' + userinfo._id + ' / public');

            // Sort the files
            var filetreexx = p5sort_files(filetreex.f);

            // Display all files and folders at this location
            for (var i in filetreexx) {
                // Figure out the name and shortname
                var f = filetreexx[i], name = f.n, shortname;
                // if (name.length > 70) { shortname = '<span title="' + EscapeHtml(name) + '">' + EscapeHtml(name.substring(0, 70)) + "..." + '</span>'; } else { shortname = EscapeHtml(name); }
                // Removed redundant filename length check because we handle it in the CSS
                shortname = EscapeHtml(name);

                // Figure out the date
                var fdatestr = '';
                if (f.d != null) { var fdate = new Date(f.d), fdatestr = printDateTime(fdate) + '&nbsp;'; }

                // Figure out the size
                var fsize = '';
                if (f.s != null) { fsize = getFileSizeStr(f.s); }

                var h = '';
                if (f.t < 3 || f.t == 4) {
                    var right = (f.t == 1 || f.t == 4) ? p5getQuotabar(f) : '', title = '';
                    h = '<div class=filelist file=999>';
                    h += '<input file=999 style=float:left name=fc class="fcb form-check-input m-1" type=checkbox onchange=p5setActions() value="' + EscapeHtml(name) + '">&nbsp;';
                    h += '<span style=float:right title="' + title + '">' + right + '</span>';
                    h += '<span title="' + shortname + '">';
                    h += '<div class="fileIcon' + f.t + ' mt-1" onclick=p5folderset("' + encodeURIComponentEx(f.nx) + '")></div>';
                    h += '<a href=# style=cursor:pointer onclick=\'return p5folderset("' + encodeURIComponentEx(f.nx) + '")\'>' + shortname + '</a>';
                    h += '</span></div>';
                } else {
                    var link = shortname, publiclink = '', loginkey = (urlargs.key) ? ('&key=' + urlargs.key) : '';
                    if (publicfolder) { 
                        publiclink = '<i class="fa-regular fa-copy fa-fw" style=cursor:pointer title="' + "Mostrar enlace público" + '" onclick=\'return p5showPublicLink("' + (publicPath + '/' + encodeURIComponentEx(f.nx)) + '?download=1' + loginkey + '")\'></i>';
                        publiclink += '<i class="fa-solid fa-download fa-fw" title="' + "Copiar enlance al portapapeles" + '" style="cursor:pointer" onclick=copyTextToClip2("' + encodeURIComponentEx(publicPath + '/' + encodeURIComponentEx(f.nx) + '?download=1' + loginkey) + '")></i>';
                    }
                    if (f.s > 0) { link = publiclink + '<a onclick=downloadFile("downloadfile.ashx?link=' + encodeURIComponentEx(filetreelinkpath + '/' + f.nx) + '")>' + shortname + '</a>'; }
                    h = '<div class=filelist file=3>';
                    h += '<input file=3 style=float:left name=fc class="fcb form-check-input m-1" type=checkbox onchange=p5setActions() value="' + f.nx + '">&nbsp;';
                    h += '<span class=fsize>' + fdatestr + '</span>';
                    h += '<span style=float:right>' + fsize + '</span>';
                    h += '<span title="' + shortname + '"><div class="fileIcon' + f.t + ' mt-1"></div>' + link + '</span>';
                    h += '</div>';
                }

                if (f.t < 3) { html1 += h; } else { html2 += h; }
            }

            //if (f.parent == null) {  }
            QH('p5rightOfButtons', p5getQuotabar(filetreex));

            QH('p5files', html1 + html2);
            QH('p5currentpath', displayPath);
            QE('p5FolderUp', filetreelocation.length != 0);
            QV('p5PublicShare', publicfolder);

            // Re-check all boxes if needed
            if (oldlinkpath == filetreelinkpath) {
                checkboxes = document.getElementsByName('fc');
                for (var i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = (checkedBoxes.indexOf(checkboxes[i].value) >= 0); }
            }

            p5setActions();
        }

        function getNiceSize(bytes) {
            if (bytes <= 0) return "El límite de almacenamiento se excede";
            if (bytes < 2048) return format("{0} bytes restantes", bytes);
            if (bytes < 2097152) return format("{0} kilobytes restantes", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("{0} megabytes restantes", Math.round(bytes / 1024 / 1024));
            return format("{0} gigabytes restantes", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize2(bytes) {
            if (bytes <= 0) return "Ninguno";
            if (bytes < 2048) return format("{0} b", bytes);
            if (bytes < 2097152) return format("{0} Kb", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("{0} Mb", Math.round(bytes / 1024 / 1024));
            return format("{0} Gb", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize3(bytes) {
            if (bytes <= 0) return "Ninguno";
            if (bytes < 2048) return format("{0} b", bytes);
            if (bytes < 2097152) return format("{0} Kb", round(bytes / 1024, 1));
            if (bytes < 2147483648) return format("{0} Mb", round(bytes / 1024 / 1024, 1));
            return format("{0} Gb", round(bytes / 1024 / 1024 / 1024, 1));
        }

        function getNetworkSpeed(bitsPerSecond) {
            if (bitsPerSecond <= 0) return "0 bps";
            if (bitsPerSecond < 1000) return format("{0} bps", bitsPerSecond);
            if (bitsPerSecond < 1000000) return format("{0} Kbps", Math.round(bitsPerSecond / 1000));
            if (bitsPerSecond < 1000000000) return format("{0} Mbps", Math.round(bitsPerSecond / 1000000));
            return format("{0} Gbps", (bitsPerSecond / 1000000000).toFixed(1));
        }

        function p5getQuotabar(f) {
            while (f.t > 1 && f.t != 4) { f = f.parent; }
            if ((f.t != 1 && f.t != 4) || (f.maxbytes == null)) return '';
            var tf = Math.floor(f.s / 1024), tq = (f.maxbytes - f.s);
            var title;
            if (f.c > 1) { title = format("{0}k en {1} archivos. {2}k máximo", tf, f.c, (Math.floor(f.maxbytes / 1024 / 1024))); } else { title = format("{0}k en 1 archivo. {1}k máximo", tf, (Math.floor(f.maxbytes / 1024 / 1024))); }
            return '<span title="' + title + '">' + getNiceSize(tq) + ' <progress style=height:10px;width:100px value=' + f.s + ' max=' + f.maxbytes + ' /></span>';
        }

        function p5showPublicLink(u) {
            setModalContent('xxAddAgent', "Enlace Público", '<input type=text style=width:350px value="' + u + '" readonly /> <i class="fa-regular fa-copy" title="' + "Copiar enlance al portapapeles" + '" style="cursor:pointer" onclick=copyTextToClip2("' + encodeURIComponentEx(u) + '")></i>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            return false;
        }

        var sortorder;
        function p5sort_filename(a, b) { if (a.ln > b.ln) return (1 * sortorder); if (a.ln < b.ln) return (-1 * sortorder); return 0; }
        function p5sort_timestamp(a, b) { if (a.d > b.d) return (1 * sortorder); if (a.d < b.d) return (-1 * sortorder); return 0; }
        function p5sort_bysize(a, b) { if (a.s == b.s) return p5sort_filename(a, b); return (((a.s - b.s)) * sortorder); }

        function p5sort_files(files) {
            var r = [], sortselection = Q('p5sortdropdown').value;
            for (var i in files) { files[i].nx = i; if (files[i].n == null) { files[i].n = i; } files[i].ln = files[i].n.toLowerCase(); r.push(files[i]); }
            sortorder = 1;
            if (sortselection > 3) { sortorder = -1; sortselection -= 3; }
            if (sortselection == 1) { r.sort(p5sort_filename); }
            else if (sortselection == 2) { r.sort(p5sort_bysize); }
            else if (sortselection == 3) { r.sort(p5sort_timestamp); }
            return r;
        }

        function p5setActions() {
            var cc = getFileSelCount(), tc = getFileCount(), sfc = getFileSelCount(false); // In order: number of entires selected, number of total entries, number of selected entires that are files (not folders)
            QE('p5DeleteFileButton', (cc > 0) && (filetreelocation.length > 0));
            QE('p5ViewFileButton', (cc == 1) && (sfc == 1) && (filetreelocation.length > 0));
            QE('p5NewFolderButton', filetreelocation.length > 0);
            QE('p5UploadButton', filetreelocation.length > 0);
            QE('p5DownloadButton', (cc > 0) && (cc == sfc) && (filetreelocation.length > 0));
            QE('p5RenameFileButton', (cc == 1) && (filetreelocation.length > 0));
            QE('p5SelectAllButton', tc > 0);
            Q('p5SelectAllButton').value = (cc > 0 ? "No seleccionar nada" : "Seleccionar Todo");
            QE('p5CutButton', (sfc > 0) && (cc == sfc));
            QE('p5CopyButton', (sfc > 0) && (cc == sfc));
            QE('p5PasteButton', (p5clipboard != null) && (p5clipboard.length > 0) && (filetreelocation.length > 0));
            [
                ['p5NewFolderButton', 'p5MobileNewFolderButton'],
                ['p5UploadButton', 'p5MobileUploadButton'],
                ['p5DownloadButton', 'p5MobileDownloadButton'],
                ['p5CutButton', 'p5MobileCutButton'],
                ['p5CopyButton', 'p5MobileCopyButton'],
                ['p5PasteButton', 'p5MobilePasteButton']
            ].forEach(function (x) { QE(x[1], !Q(x[0]).disabled); });
        }

        function getFileSelCount(includeDirs) { var cc = 0, checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && ((includeDirs != false) || (checkboxes[i].attributes.file.value == '3'))) cc++; } return cc; }
        function getFileSelDirCount() { var cc = 0, checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && (checkboxes[i].attributes.file.value == '999')) cc++; } return cc; }
        function getFileCount() { var cc = 0; var checkboxes = document.getElementsByName('fc'); return checkboxes.length; }
        function p5selectallfile() { var nv = (getFileSelCount() == 0), checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = nv; } p5setActions(); }
        function setupBackPointers(x) { if (x.f != null) { var fs = 0, fc = 0; for (var i in x.f) { setupBackPointers(x.f[i]); x.f[i].parent = x; if (x.f[i].s) { fs += x.f[i].s; } if (x.f[i].c) { fc += x.f[i].c; } if (x.f[i].t == 3) { fc++; } } x.s = fs; x.c = fc; } return x; }
        function getFileSizeStr(size) { if (typeof size != 'number') { size = 0; } if (size == 1) return "1 byte"; return format("{0} bytes", size); }
        function p5folderup(x) { if (x == null) { filetreelocation.pop(); } else { while (filetreelocation.length > x) { filetreelocation.pop(); } } updateFiles(); return false; }
        function p5folderset(x) { filetreelocation.push(decodeURIComponent(x)); updateFiles(); return false; }
        function p5createfolder() {
            setModalContent('xxAddAgent', "Nueva Carpeta", '<input type=text id=p5renameinput class="form-control" maxlength=64 onkeyup=p5fileNameCheck(event) />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p5createfolderEx);
            focusTextBox('p5renameinput');
            p5fileNameCheck();
        }
        function p5createfolderEx() { meshserver.send({ action: 'fileoperation', fileop: 'createfolder', path: filetreelocation, newfolder: Q('p5renameinput').value }); }
        function p5deletefile() {
            var cc = getFileSelCount(), rec = (getFileSelDirCount() > 0) ? '<br /><br /><label><input type=checkbox class="form-check-input me-2" id=p5recdeleteinput>' + "Eliminado recursivo" + '</label><br>' : '<input type=checkbox id=p5recdeleteinput class="form-check-input me-2" style=\'display:none\'>';
            setModalContent('xxAddAgent', "Eliminar", (cc > 1) ? (format("¿Eliminar {0} elementos seleccionados?", cc) + rec) : ("¿Eliminar elemento seleccionado?" + rec));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p5deletefileEx);
        }
        function p5deletefileEx() { var delfiles = [], checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { delfiles.push(checkboxes[i].value); } } meshserver.send({ action: 'fileoperation', fileop: 'delete', path: filetreelocation, delfiles: delfiles, rec: Q('p5recdeleteinput').checked }); }
        function p5renamefile() {
            var renamefile, checkboxes = document.getElementsByName('fc');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) { renamefile = checkboxes[i].value; }
            }
            setModalContent('xxAddAgent', "Renombrar", '<input type=text id=p5renameinput class="form-control" maxlength=64 onkeyup=p5fileNameCheck(event) value="' + renamefile + '" />');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => { meshserver.send({ action: 'fileoperation', fileop: 'rename', path: filetreelocation, oldname: renamefile, newname: Q('p5renameinput').value}); });
            focusTextBox('p5renameinput'); p5fileNameCheck();
        }
        function p5fileNameCheck(e) { var x = isFilenameValid(Q('p5renameinput').value); QE('idx_dlgOkButton', x); if ((x == true) && (e && e.keyCode == 13)) { dialogclose(1); } }
        var isFilenameValid = (function () { var x1 = /^[^\\/:\*\?"<>\|]+$/, x2 = /^\./, x3 = /^(nul|prn|con|lpt[0-9]|com[0-9])(\.|$)/i; return function isFilenameValid(fname) { return x1.test(fname) && !x2.test(fname) && !x3.test(fname) && (fname[0] != '.'); } })();
        function p5uploadFile() {
            setModalContent('xxAddAgent', "Cargar Archivo", '<form method=post enctype=multipart/form-data action=uploadfile.ashx target=fileUploadFrame><input type=text name=link style=display:none id=p5uploadpath value="' + encodeURIComponentEx(filetreelinkpath) + '" /><input type=file name=files id=p5uploadinput class="form-control" multiple=multiple onchange="p5updateUploadDialogOk(\'p5uploadinput\')" /><input type=hidden name=authCookie value=' + authCookie + ' /><input type=submit id=p5loginSubmit class="btn btn-outline-success mt-2" style="display:none" /><span id=p5confirmOverwriteSpan style=display:none><br/><label><input type=checkbox class="form-check-input me-2" id=p5confirmOverwrite onchange="p5updateUploadDialogOk(\'p5uploadinput\')" />' + "¿Confirmar sobrescritura?" + '</label></span></ >');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p5uploadFileEx);
            p5updateUploadDialogOk('p5uploadinput');
        }
        function p5uploadFileEx() { Q('p5loginSubmit').click(); }
        function p5GetFileInfo(name) { var filetreex = filetree; for (var i in filetreelocation) { if ((filetreex.f != null) && (filetreex.f[filetreelocation[i]] != null)) { filetreex = filetreex.f[filetreelocation[i]]; } } return filetreex.f[name]; }
        function p5updateUploadDialogOk() {
            // Check if these are files we can upload, remove all folders.
            var xallfiles = Q('p5uploadinput').files, files = [];
            for (var i in xallfiles) { if ((xallfiles[i].size != null) && (xallfiles[i].size != 0)) { files.push(xallfiles[i]); } }

            // Check if these files are duplicates of existing files.
            var filetreex = filetree, allfiles = [], overWriteCount = 0;
            for (var i in filetreelocation) {
                if ((filetreex.f != null) && (filetreex.f[filetreelocation[i]] != null)) { filetreex = filetreex.f[filetreelocation[i]]; }
            }
            QE('idx_dlgOkButton', xallfiles.length > 0);
            if (xallfiles.length > 0) {
                if (filetreex.f != null) {
                    for (var i in filetreex.f) { allfiles.push(i); }
                    for (var i = 0; i < xallfiles.length; i++) {
                        if (allfiles.indexOf(xallfiles[i].name) >= 0) { overWriteCount++; } // TODO: If the server is Windows, we need to lowercase both names.
                    }
                }
                QV('p5confirmOverwriteSpan', overWriteCount > 0);
                if (overWriteCount > 0) {
                    QE('idx_dlgOkButton', Q('p5confirmOverwrite').checked);
                } else {
                    Q('p5confirmOverwrite').checked = false;
                    QE('idx_dlgOkButton', true);
                }
            }
        }
        function p5viewfile() {
            var checkboxes = document.getElementsByName('fc');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    var f = p5GetFileInfo(checkboxes[i].value); if (f == null) return;
                    if (f.s <= 204800) { meshserver.send({ action: 'fileoperation', fileop: 'get', path: filetreelocation, file: checkboxes[i].value, tag: 'edit' }); } else { messagebox("Editor de Archivos", "Solo se pueden editar archivos de menos de 200k."); }
                    return;
                }
            }
        }
        var p5clipboard = null, p5clipboardFolder = null, p5clipboardCut = 0;
        function p5copyFile(cut) {
            var checkboxes = document.getElementsByName('fc'); p5clipboard = []; p5clipboardCut = cut, p5clipboardFolder = Clone(filetreelocation);
            for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && (checkboxes[i].attributes.file.value == '3')) { p5clipboard.push(checkboxes[i].value); } }
            p5updateClipview();
        }
        function p5pasteFile() {
            var x = ''; if ((p5clipboard != null) && (p5clipboard.length > 0)) { x = format("¿Confirmar {0} de {1} entrada{2} a esta ubicación?", (p5clipboardCut == 0 ? 'copy' : 'move'), p5clipboard.length, ((p5clipboard.length > 1) ? 's' : '')) }
            setModalContent('xxAddAgent', "Pegar", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p5pasteFileEx);

        }
        function p5pasteFileEx() { meshserver.send({ action: 'fileoperation', fileop: (p5clipboardCut == 0 ? 'copy' : 'move'), scpath: p5clipboardFolder, path: filetreelocation, names: p5clipboard }); p5folderup(999); if (p5clipboardCut == 1) { p5clipboard = null, p5clipboardFolder = null, p5clipboardCut = 0; p5updateClipview(); } }
        function p5updateClipview() { var x = ''; if ((p5clipboard != null) && (p5clipboard.length > 0)) { x = format("Manteniendo {0} entrada{1} para {2}", p5clipboard.length, ((p5clipboard.length > 1) ? 's' : ''), (p5clipboardCut == 0 ? "Copiar" : "Move")) + ', <a href=# onclick="return p5clearClip()" style=cursor:pointer>' + "Eliminar" + '</a>.' } QH('p5bottomstatus', x); p5setActions(); }
        function p5clearClip() { p5clipboard = null; p5clipboardFolder = null; p5clipboardCut = 0; p5updateClipview(); return false; }

        function p5fileDragDrop(e) {
            if (xxdialogMode) return;
            haltEvent(e);
            QV('bigfail', false);
            QV('bigok', false);
            //QV('p5fileCatchAllInput', false);

            // Check if these are files we can upload, remove all folders.
            if (e.dataTransfer == null) return;
            var files = [];
            for (var i in e.dataTransfer.files) { if ((e.dataTransfer.files[i].size != null) && (e.dataTransfer.files[i].size != 0)) { files.push(e.dataTransfer.files[i]); } }
            if (files.length == 0) return;

            // Check if these files are duplicates of existing files.
            var filetreex = filetree, allfiles = [], overWriteCount = 0;
            for (var i in filetreelocation) {
                if ((filetreex.f != null) && (filetreex.f[filetreelocation[i]] != null)) { filetreex = filetreex.f[filetreelocation[i]]; }
            }
            if (filetreex.f != null) {
                for (var i in filetreex.f) { allfiles.push(i); }
                for (var i = 0; i < e.dataTransfer.files.length; i++) {
                    if (allfiles.indexOf(e.dataTransfer.files[i].name) >= 0) { overWriteCount++; } // TODO: If the server is Windows, we need to lowercase both names.
                }
            }

            if (overWriteCount == 0) {
                // If no overwrite, go ahead with upload
                p5PerformUpload(1, e.dataTransfer.files);
            } else {
                // Otherwise, prompt for confirmation
                setModalContent('xxAddAgent', "Cargar Archivo", format((overWriteCount == 1) ? "La carga sobrescribirá 1 archivo. ¿Continuar?" : "La carga sobrescribirá {0} archivos. ¿Continuar?", overWriteCount));
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p5PerformUpload(3, e.dataTransfer.files));
            }
        }

        function p5PerformUpload(b, files) {
            // For Chrome & Firefox
            var error = 0;
            p5uploadFile(); // Display the the dialog box
            try { Q('p5uploadinput').files = files; } catch (ex) { error = 1; } // Set the files in the dialog box
            if (error == 0) { p5uploadFileEx(); } // Press the submit button
            setDialogMode(0); // Close the dialog box

            // For IE browser - This will not work with very large files
            if (error == 1) {
                if (filetreelocation.length == 0) return;
                var names = [], sizes = [], types = [], datas = [], readercount = files.length, totalSize = 0;
                for (var i = 0; i < files.length; i++) { totalSize += files[i].size; }
                if (totalSize > 1300000) { p5uploadFile(); return; } // File is too large, not sure what the real maximum is.
                for (var i = 0; i < files.length; i++) {
                    var reader = new FileReader(), file = files[i];
                    names.push(file.name);
                    sizes.push(file.size);
                    types.push(file.type);
                    reader.onload = function (event) {
                        datas.push(event.target.result);
                        if (--readercount == 0) {
                            Q('p5fileDragName').value = names.join('*');
                            Q('p5fileDragSize').value = sizes.join('*');
                            Q('p5fileDragType').value = types.join('*');
                            Q('p5fileDragData').value = datas.join('*'); // This will not work for large files, there is a limit on the data size in a field.
                            Q('p5fileDragLink').value = encodeURIComponentEx(filetreelinkpath);
                            Q('p5fileDragAuthCookie').value = authCookie;
                            Q('p5loginSubmit2').click();
                        }
                    }
                    reader.readAsDataURL(file);
                }
            }
        }

        var p5dragtimer = null;
        function p5fileDragOver(e) {
            if (xxdialogMode) return;
            haltEvent(e);
            if (p5dragtimer != null) { clearTimeout(p5dragtimer); p5dragtimer = null; }
            var ac = true; // TODO: Set to true if we can accept the file
            if (filetreelocation.length == 0) { ac = false; }
            QV('bigok', ac);
            QV('bigfail', !ac);
            //QV('p5fileCatchAllInput', ac);
        }

        function p5fileDragLeave(e) {
            if (xxdialogMode) return;
            haltEvent(e);
            if (e.target.id != 'p5filetable') {
                QV('bigfail', false);
                QV('bigok', false);
                //QV('p5fileCatchAllInput', false);
            } else {
                p5dragtimer = setTimeout(function () { QV('bigfail', false); QV('bigok', false); p5dragtimer = null; }, 10);
            }
        }

        function p5downloadButton() {
            var checkboxes = document.getElementsByName('fc');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    var link = 'downloadfile.ashx?link=' + encodeURIComponentEx(filetreelinkpath + '/' + checkboxes[i].value);
                    downloadFile(link);
                }
            }
        }

        /*
        function p5fileCatchAllInputChanged(e) {
            p5fileDragLeave(e);
            Q('p5fileDragLink2').value = encodeURIComponentEx(filetreelinkpath);
            Q('p5fileCatchAllSubmit').click();
        }
        */

        //
        // MY EVENTS
        //

        // Events
        var eventsMessageId = {
            1: "Cuenta de Ingreso",
            2: "Cerrar sesión de cuenta",
            3: "Idioma cambiado de {1} a {2}",
            4: "Se unió a la sesión multiplex de escritorio", // No longer in use, replaced with 143
            5: "Dejó la sesión multiplex de escritorio", // No longer in use, replaced with 144
            6: "Sesión multiplex de escritorio iniciada", // No longer in use, replaced with 145
            7: "Grabación de la sesión finalizada, {0} segundo(s)", // No longer in use, replaced with 146
            8: "Sesión multiplex de escritorio cerrada, {0} segundo(s)", // No longer in use, replaced with 147
            9: "Sesión de relay finalizada \"{0}\" de {1} a {2}, {3} segundo(s)",
            10: "Sesión de terminal finalizada \"{0}\" de {1} a {2}, {3} segundo(s)",
            11: "Sesión de escritorio finalizada \"{0}\" de {1} a {2}, {3} segundo(s)",
            12: "Sesión de gestión de archivos finalizada \"{0}\" de {1} a {2}, {3} segundo(s)",
            13: "Sesión de retransmisión iniciada \"{0}\" de {1} a {2}",
            14: "Sesión de terminal iniciada \"{0}\" de {1} a {2}",
            15: "Sesión de escritorio iniciada \"{0}\" de {1} a {2}",
            16: "Sesión de administración de archivos iniciada \"{0}\" de {1} a {2}",
            17: "Procesando comando de consola: \"{0}\"",
            18: "Mostrando cuadro de mensaje, título = \"{0}\", mensaje = \"{1}\"",
            19: "Finalización del proceso {0} ({1})",
            20: "Apertura: {0}",
            21: "Obteniendo contenido del portapapeles, {0} byte(s)",
            22: "Configuración del contenido del portapapeles, {0} byte(s)",
            23: "Intentando la activación del modo Intel (R) AMT ACM",
            24: "Ejecutando comandos",
            25: "Realizando acción de energía = {0}, forzado = {1}",
            26: "Mostrando mensaje de brindis, título = \"{0}\", mensaje = \"{1}\"",
            27: "Solicitud de terminal remota aceptada por el usuario local",
            28: "Solicitud de terminal remota rechazada por el usuario local",
            29: "Conexión de escritorio remoto cerrada con fuerza por el usuario local",
            30: "Iniciar el escritorio remoto después de que el usuario local aceptó",
            31: "Barra de Conexión de Escritorio Remoto Activada/Actualizada",
            32: "La barra de conexión de escritorio remoto falla o no es compatible",
            33: "Conexión de escritorio remoto cerrada con fuerza por el usuario local", // No longer used, 29 must be used instead.
            34: "No se pudo iniciar el escritorio remoto después de que el usuario local rechazó",
            35: "Escritorio remoto iniciado con notificación flotante (Toast)",
            36: "Escritorio remoto iniciado sin notificación",
            37: "Barra de Conexión de Escritorio Remoto Activada/Actualizada", // No longer used, 31 must be used instead.
            38: "La Barra de Conexión de Escritorio Remoto Falló o no es Compatible", // No longer used, 33 must be used instead.
            39: "Conexión de escritorio remoto cerrada con fuerza por el usuario local", // No longer used, 29 must be used instead.
            40: "Iniciar archivos remotos después de que el usuario local aceptó",
            41: "Error al iniciar archivos remotos después de que el usuario local rechazó",
            42: "Archivos remotos iniciados con notificación de tributos",
            43: "Archivos remotos iniciados sin notificación",
            44: "Crear carpeta: \"{0}\"",
            45: "Eliminar: \"{0}\"",
            46: "Eliminar recursivo: \"{0}\", {1} elemento(s) eliminado",
            47: "Eliminar: \"{0}\", {1} elemento(s) eliminado",
            48: "Cambiar Nombre: \"{0}\" a \"{1}\"",
            49: "Descargar: \"{0}\"",
            50: "Subir: \"{0}\"",
            51: "Copiar: \"{0}\" a \"{1}\"",
            52: "Mover: \"{0}\" a \"{1}\"",
            53: "Bloquear al usuario remoto fuera del escritorio",
            54: "El agente cerró la sesión con {0}% de compresión de agente a servidor. Enviado: {1}, comprimido: {2}",
            55: "Grupo de dispositivos creado: {0}",
            56: "Grupo de dispositivos recuperado: {0}",
            57: "Dispositivo agregado {0} al grupo de dispositivos {1}",
            58: "Dispositivo solicitado activación de Intel (R) AMT ACM, FQDN: {0}",
            59: "Dispositivo cambiado {0} del grupo {1}: {2}",
            60: "Derechos de dispositivo de usuario eliminados para {0}",
            61: "Derechos de dispositivo de usuario modificados para {0}",
            62: "Usuario eliminado {0} del grupo de usuarios {1}",
            63: "Cuenta eliminada",
            64: "Cuenta creada, el nombre de usuario es {0}",
            65: "Cuenta creada, el correo electrónico es {0}",
            66: "Cuenta cambiada: {0}",
            67: "La membresía del grupo de usuarios cambió: {0}",
            68: "Se agregó el grupo de usuarios {0} al grupo de dispositivos {1}",
            69: "Grupo de usuarios creado: {0}",
            70: "Se quitó el grupo de usuarios {0} del grupo de dispositivos {1}",
            71: "Usuario agregado {0} al grupo de usuarios {1}",
            72: "Usuario eliminado {0} del grupo de usuarios {1}",
            73: "La notificación del grupo de dispositivos cambió",
            74: "Se cambió la contraseña de la cuenta: {0}",
            75: "Credenciales de cuenta cambiadas",
            76: "Grupo de dispositivos creado: {0}",
            77: "Grupo de dispositivos eliminado: {0}",
            78: "La membresía del grupo de dispositivos cambió: {0}",
            79: "El grupo de usuarios cambió: {0}",
            80: "Usuario agregado {0} al grupo de usuarios {1}",
            81: "Derechos de dispositivo de usuario eliminados para {0}",
            82: "Derechos de dispositivo de usuario modificados para {0}",
            83: "Se eliminó al usuario {0} del grupo de dispositivos {1}",
            84: "Dispositivo agregado {0} al grupo de dispositivos {1}",
            85: "Se movió el dispositivo {0} al grupo {1}",
            86: "Derechos de dispositivo de usuario eliminados para {0}",
            87: "Dispositivo eliminado {0} del grupo de dispositivos {1}",
            88: "Autenticación de correo electrónico de dos factores habilitada",
            89: "Autenticación de dos factores de correo electrónico deshabilitada",
            90: "Aplicación de autenticación agregada",
            91: "Aplicación de autenticación eliminada",
            92: "Se generaron nuevos códigos de respaldo 2FA",
            93: "Códigos de seguridad 2FA borrados",
            94: "Llave de seguridad eliminada",
            95: "Llave de seguridad agregada",
            96: "Número de teléfono verificado del usuario {0}",
            97: "Número de teléfono eliminado del usuario {0}",
            98: "Ayuda Solicitada, usuario: {0}, detalles: {1}",
            99: "Ejecutando comandos como usuario",
            100: "Ejecutar comandos como usuario si es posible",
            101: "Se agregó el recurso compartido de dispositivo {0} de {1} a {2}",
            102: "Compartir dispositivo eliminado {0}",
            103: "Carga por lotes de {0} archivo (s) a la carpeta {1}",
            104: "Descarga automatizada del archivo de volcado del núcleo del agente: \"{0}\"",
            105: "Subir: \"{0}\", Tamaño: {1}",
            106: "Descarga: \"{0}\", Tamaño: {1}",
            107: "Acceso a la cuenta de {0}, {1}, {2}",
            108: "Intento de inicio de sesión del usuario con un segundo factor incorrecto de {0}, {1}, {2}",
            109: "Intento de inicio de sesión del usuario en una cuenta bloqueada de {0}, {1}, {2}",
            110: "Intento de inicio de sesión de usuario no válido de {0}, {1}, {2}",
            111: "Dispositivo solicitado activación Intel (R) AMT ACM TLS, FQDN: {0}",
            112: "Sesión de mensajería finalizada \"{0}\" de {1} a {2}, {3} segundo (s)",
            113: "Dispositivo de autenticación de notificaciones push agregado",
            114: "Dispositivo de autenticación de notificaciones push eliminado",
            115: "Token de inicio de sesión agregado",
            116: "Token de inicio de sesión eliminado",
            117: "Esta es una versión antigua del agente, considera actualizar.",
            118: "Este agente tiene un mecanismo de validación de certificados obsoleto, considera la posibilidad de actualizarlo.",
            119: "Este agente está utilizando túneles inseguros, considera actualizar.",
            120: "Sesión de relé local iniciada \"{0}\", protocolo {1} a {2}",
            121: "Sesión de relay local finalizada \"{0}\", protocolo {1} a {2}, {3} segundo(s)",
            122: "Abandonar la sesión de escritorio multiplexada tras {0} segundo(s).", // No longer in use, replaced with 144
            123: "Salió de la sesión Web-SSH \"{1}\" después de {0} segundo(s).",
            124: "Salió de la sesión Web-SFTP \"{1}\" después de {0} segundo(s).",
            125: "Salió de la sesión Web-RDP \"{1}\" después de {0} segundo(s).",
            126: "Abandonar la sesión Web-VNC tras {0} segundo(s).",
            127: "Cambiado el nombre visible de la cuenta a {0}.",
            128: "Cuenta creada, el nombre es {0}.",
            129: "Eliminado el nombre visible de la cuenta.",
            130: "Han cambiado las notificaciones del usuario",
            131: "Dispositivo compartido {0} agregado con tiempo ilimitado.",
            132: "Encender.",
            133: "Apagar.",
            134: "Sesión de escritorio desconectada a la fuerza del usuario {0}",
            135: "Sesión de terminal desconectada a la fuerza del usuario {0}",
            136: "Sesión de archivos desconectada a la fuerza del usuario {0}",
            137: "Sesión de enrutamiento desconectada a la fuerza del usuario {0}",
            138: "Dispositivo compartido compartido {0} recurrente diariamente.",
            139: "Dispositivo compartido {0} recurrente semanalmente.",
            140: "Dispositivo cambiado {0} del grupo {1}: {2}",
            141: "Cambio de política de Intel(r) AMT",
            142: "Se cambió el grupo de dispositivos {0}: {1}",
            143: "Se unió a la sesión múltiplex de escritorio \"{0}\"",
            144: "Salió de la sesión multiplex de escritorio \"{0}\" después de {1} segundo(s).",
            145: "Sesión múltiplex de escritorio iniciada \"{0}\"",
            146: "Sesión de grabación finalizada \"{0}\", {1} segundo(s)",
            147: "Sesión múltiplex de escritorio cerrada \"{0}\", {1} segundo(s)",
            148: "Sesión Web-SSH iniciada \"{0}\".",
            149: "Sesión de Web-SFTP iniciada \"{0}\".",
            150: "Sesión Web-RDP iniciada \"{0}\".",
            151: "Sesión Web-VNC iniciada \"{0}\".", // Not in use yet
            152: "Ya no es un repetidor para \"{0}\".",
            153: "Es un relé para \"{0}\".",
            154: "La cuenta cambio a ser actualizada con los datos de LDAP.",
            155: "Se denego el inicio de sesion de {0}, {1}, {2}",
            156: "Se verifico la cuenta de Mensajes de {0}",
            157: "Removio cuenta de mensajes del usuario {0}",
            158: "Displaying alert box, title=\"{0}\", message=\"{1}\"",
            159: "Device Powered On",
            160: "Enabled Duo two-factor authentication",
            161: "Disabled Duo two-factor authentication",
            162: "Sesión de mensajería iniciada \"{0}\" de {1} a {2}",
            163: "Dispositivo eliminado {0} del grupo de usuarios {1}",
            164: "Crear archivo: \"{0}\""
        };

        var eventsShortMessageId = {
            1: "Nombre del grupo",
            2: "Descripción",
            3: "Banderas",
            4: "Consentimiento",
            5: "Auto-Remover",
            6: "Codigo de invitacion",
            7: "dispositivo de retransmisión"
        }

        // Highlights the device being hovered
        function eventMouseHover(e, over) {
            e.children[1].classList.remove('g1s');
            e.children[2].classList.remove('style10s');
            //e.children[2].style['background-color'] = ((over == 0) ? '#c9c9c9' : '#b9b9b9');
            if (over == 1) { e.children[1].classList.add('g1s'); e.children[2].classList.add('style10s'); }
        }

        function eventsUpdate() {
            var x = '', dateHeader = null;
            for (var i in events) {
                var event = events[i], time = new Date(event.time);
                if (event.msg) {
                    if (event.h == null) { event.h = Math.random(); }
                    if (printDate(time) != dateHeader) {
                        if (dateHeader != null) x += '</table>';
                        dateHeader = printDate(time);
                        x += '<table class="table table-hover p3eventsTable" cellpadding=0 cellspacing=0><tr><td colspan=4 class=DevSt>' + dateHeader + '</td></tr>';
                    }
                    var icon = 'fa-mobile-screen';
                    if (event.etype == 'ugrp') icon = 'fa-users';
                    if (event.etype == 'user') icon = 'fa-user';
                    if (event.etype == 'server') icon = 'fa-server';

                    var msg;
                    if ((event.msgid == null) || (eventsMessageId[event.msgid] == null)) {
                        if (typeof event.msg == 'string') { msg = EscapeHtml(event.msg).split('(R)').join('&reg;'); } else { msg = ''; }
                    } else {
                        msg = eventsMessageId[event.msgid];
                        if (event.msgArgs != null) {
                            for (var i in event.msgArgs) {
                                var xx = event.msgArgs[i];
                                if (Array.isArray(xx)) {
                                    var x2 = [];
                                    for (var j in xx) { if ((typeof xx[j] == 'number') && (eventsShortMessageId[xx[j]])) { x2.push(eventsShortMessageId[xx[j]]); } else { x2.push(xx[j]); } }
                                    xx = x2.join(", ");
                                } else {
                                    if ((typeof xx == 'string') && (xx.indexOf('DATETIME:') == 0)) { xx = printFlexDateTime(new Date(parseInt(xx.substring(9)))); }

                                }
                                msg = msg.split('{' + i + '}').join(xx);
                            }
                        }
                        msg = EscapeHtml(msg).split('(R)').join('&reg;');
                    }
                    if (event.nodeid) {
                        var node = getNodeFromId(event.nodeid);
                        if (node != null) {
                            switch(node.icon){
                                case 1:
                                    icon = 'fa-computer';
                                    break;
                                case 2:
                                    icon = 'fa-laptop';
                                    break;
                                case 8:
                                    icon = 'fa-laptop-code';
                                    break;
                            }
                            msg = '<a href=# onclick=\'gotoDevice("' + event.nodeid + '",10);haltEvent(event);\'>' + EscapeHtml(node.name) + '</a> &rarr; ' + msg;
                        }
                    }
                    if (event.username) {
                        var guestname = '';
                        if (event.guestname) { guestname = ' / <span title="' + "Esta es una sesión para compartir con invitados" + '">' + EscapeHtml(event.guestname) + '</span>'; }
                        if ((userinfo.siteadmin & 2) && (event.userid)) {
                            msg = '<a href=# onclick=\'gotoUser("' + encodeURIComponentEx(event.userid) + '");haltEvent(event);\'>' + EscapeHtml(event.username) + '</a>' + guestname + ' &rarr; ' + msg;
                        } else {
                            msg = EscapeHtml(event.username) + guestname + ' &rarr; ' + msg;
                        }
                    }
                    if (event.remoteaddr) { msg += ' (' + event.remoteaddr + ')'; }
                    if (event.etype == 'relay' || event.action == 'relaylog') icon = 'fa-arrow-right-arrow-left';
                    x += '<tr onclick=showEventDetails(' + event.h + ',2)  onmouseover=eventMouseHover(this,1) onmouseout=eventMouseHover(this,0) role=button><td style=width:18px><i class="fa-fw fa-solid ' + icon + '"></i></td><td class=g1>&nbsp;</td><td class=style10>' + printTime(time) + ' - ' + msg + '</td></tr><tr></tr>';
                }
            }
            if (dateHeader != null) x += '</table>';
            if (x == '') x = '<br><i>' + "No se Encontraron Eventos" + '</i><br><br>';
            QH('p3events', x);
        }

        function refreshEvents() {
            if (p3filterevents.value != "") {
                meshserver.send({ action: 'events', limit: parseInt(p3limitdropdown.value), filter: p3filterevents.value });
            } else {
                meshserver.send({ action: 'events', limit: parseInt(p3limitdropdown.value) });
            }
        }

        function p3showReportDialog() {
            if (xxdialogMode) return;
            var x = '<div style=float:right><img src=\'images/report60.png\' height=74 width=60 style=padding-top:2px /></div><div>';
            x += addHtmlFormFloating("Tipo de Reporte", '<select id=d2reportType class="form-select" onchange=p3showReportValidation()><option value=1>' + "Todos los Eventos" + '</option>');
            x += addHtmlFormFloating("Espacio de Tiempo", '<select id=d2reportSpan class="form-select" onchange=p3showReportValidation()><option value=1>' + "Todo Disponible" + '</option><option value=2>' + "Un Día" + '</option></select>');
            x += '<div id=d2daySelector>' + addHtmlFormFloating("Reporte del Día", '<input type=date id=d2reportDay class="form-control" onchange=p3showReportValidation()></input>') + '</div>';
            x += addHtmlFormFloating("Agrupar Por", '<select id=d2reportGroupBy class="form-select" onchange=p3showReportValidation()><option value=1>' + "Ninguno" + '</option><option value=2>' + "Grupo de Dispositivos" + '</option><option value=3>' + "Usuario" + '</option>');
            x += addHtmlFormFloating("Formato", '<select id=d2reportFormat class="form-select" onchange=p3showReportValidation()><option value=1>' + "CSV" + '</option><option value=2>' + "JSON" + '</option>');
            x += '</div>';
            setModalContent('xxAddAgent', "Descargar Reporte", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            p3showReportValidation();
            Q('d2reportType').focus();
        }

        function p3showReportValidation() {
            var reportType = Q('d2reportType').value;
            var reportSpan = Q('d2reportSpan').value;
            var reportDay = Q('d2reportDay').value;
            var reportGroupBy = Q('d2reportGroupBy').value;
            var reportFormat = Q('d2reportFormat').value;
            QV('d2daySelector', reportSpan == 2);
            //console.log(reportType, reportSpan, reportDay, reportGroupBy, reportFormat);

            var ok = true;
            if ((reportSpan == 2) && (reportDay == '')) ok = false;
            QE('idx_dlgOkButton', ok);
        }

        function p3showDownloadEventsDialog(mode) {
            if (xxdialogMode) return;
            var x = "Descargar la lista de eventos con uno de los formatos de archivo a continuación." + '<br /><br />';
            x += addHtmlValue("Formato CSV", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogCSV(' + mode + ')">' + 'eventslist.csv' + '</a>');
            x += addHtmlValue("Formato JSON", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogJSON(' + mode + ')">' + 'eventslist.json' + '</a>');
            setModalContent('xxAddAgent', "Exportar Lista de Eventos", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

        function p3downloadEventsDialogCSV(mode) {
            var csv, eventList;
            if (mode == 1) { eventList = currentDeviceEvents; }
            if (mode == 2) { eventList = events; }
            if (mode == 3) { eventList = currentUserEvents; }
            csv = "utc, time, type, action, user, device, message" + '\r\n';
            for (var i in eventList) {
                var nodename = '';
                if (eventList[i].nodeid) { var node = getNodeFromId(eventList[i].nodeid); if (node && node.name) { nodename = node.name; } }
                csv += '"' + eventList[i].time + '","' + printDateTime(new Date(eventList[i].time)) + '","' + eventList[i].etype + '","' + ((eventList[i].action != null) ? eventList[i].action : '') + '","' + ((eventList[i].username != null) ? eventList[i].username : '') + '","' + EscapeHtml(nodename) + '","' + ((eventList[i].msg != null) ? eventList[i].msg : '').split(',').join(' -') + '"\r\n';
            }
            saveAs(stringToUtf8Blob(csv), 'eventslist.csv');
            return false;
        }

        function p3downloadEventsDialogJSON(mode) {
            var r = [], eventList;
            if (mode == 1) { eventList = currentDeviceEvents; }
            if (mode == 2) { eventList = events; }
            if (mode == 3) { eventList = currentUserEvents; }
            for (var i in eventList) { r.push(events[i]); }
            saveAs(new Blob([JSON.stringify(r, null, 2)], { type: 'application/octet-stream' }), 'eventslist.json');
            return false;
        }

        //
        // MY USERS
        //

        function updateUsers() {
            QV('UserNewAccountButton', ((features & 4) == 0) && (serverinfo.domainauth == false));
            if ((users == null) || ((features & 4) != 0)) { QH('p3users', ''); return; }

            // Get the number of users we can display
            var displayUsersLimit = ((userViewSettings != null) && (userViewSettings.noViewLimit)) ? 100000 : 100;

            // Sort the list of user id's
            var sortedUsers = [], maxUsers = displayUsersLimit, hiddenUsers = 0;
            for (var i in users) { sortedUsers.push(users[i]); }
            sortedUsers.sort(nameSort);

            // Get search
            var userSearch = Q('UserSearchInput').value.toLowerCase();
            var emailSearch = userSearch;
            if (userSearch.startsWith('email:')) { userSearch = null; emailSearch = emailSearch.substring(6); }
            else if (userSearch.startsWith('name:')) { emailSearch = null; userSearch = userSearch.substring(5); }
            else if (userSearch.startsWith('e:')) { userSearch = null; emailSearch = emailSearch.substring(2); }
            else if (userSearch.startsWith('n:')) { emailSearch = null; userSearch = userSearch.substring(2); }

            // Display the users using the sorted list
            var x = '<table class="table table-hover" cellpadding=0 cellspacing=0>', addHeader = true;
            x += '<th>' + "Nombre" + '<th style=width:80px>' + "Grupos de Dispositivos" + '<th style=width:120px>' + nobreak("Último Acceso") + '<th style=width:120px>' + "Permisos";

            // Save the list of currently checked users
            var checkedUserids = [], elements = document.getElementsByClassName('UserCheckbox');
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked) { checkedUserids.push(elements[i].value); } }

            // Online users
            for (var i in sortedUsers) {
                var user = sortedUsers[i], sessions = null;
                if (wssessions != null) { sessions = wssessions[user._id]; }
                if ((sessions != null) &&
                    ((userSearch != null) && ((userSearch == '') || (user.name.toLowerCase().indexOf(userSearch) >= 0)) ||
                        ((emailSearch != null) && ((user.email != null) && (user.email.toLowerCase().indexOf(emailSearch) >= 0))))
                ) {
                    if (maxUsers > 0) {
                        if (addHeader) { x += '<tr><td class=userTableHeader colspan=4>' + "Usuarios en Línea"; addHeader = false; }
                        x += addUserHtml(user, sessions);
                        maxUsers--;
                    } else {
                        hiddenUsers++;
                    }
                }
            }
            addHeader = true;
            // Offline users
            for (var i in sortedUsers) {
                var user = sortedUsers[i], sessions = null;
                if (wssessions != null) { sessions = wssessions[user._id]; }
                if ((sessions == null) &&
                    ((userSearch != null) && ((userSearch == '') || (user.name.toLowerCase().indexOf(userSearch) >= 0)) ||
                        ((emailSearch != null) && ((user.email != null) && (user.email.toLowerCase().indexOf(emailSearch) >= 0))))
                ) {
                    if (maxUsers > 0) {
                        if (addHeader) { x += '<tr><td class=userTableHeader colspan=4>' + "Usuarios fuera de línea"; addHeader = false; }
                        x += addUserHtml(user, sessions);
                        maxUsers--;
                    } else {
                        hiddenUsers++;
                    }
                }
            }
            x += '</table>';
            if (hiddenUsers == 1) { x += '<br />' + "1 usuario adicional no se está mostrando, usa el cuadro de búsqueda para localizar usuarios..." + '<br />'; }
            else if (hiddenUsers > 1) { x += '<br />' + format("{0} usuarios adicionales no mostrados, usa el cuadro de búsqueda para encontrar los usuarios...", hiddenUsers) + '<br />'; }
            if (maxUsers == displayUsersLimit) { x += '<br />' + "No se encontraron usuarios." + '<br />'; }
            QH('p3users', x);

            // Re-check userid's
            elements = document.getElementsByClassName('UserCheckbox');
            var eself = encodeURIComponentEx(userinfo._id);
            for (var i = 0; i < elements.length; i++) { elements[i].checked = ((checkedUserids.indexOf(elements[i].value) >= 0) && (elements[i].value != eself)); }
            p3updateInfo();

            // Update current user panel if needed
            if ((currentUser != null) && (xxcurrentView == 30)) { gotoUser(encodeURIComponentEx(currentUser._id), true); }
        }

        function addUserHtml(user, sessions) {
            var x = '', gray = ' gray', msg = '', self = (user._id != userinfo._id), lastAccess = '', permissions = '';
            if (sessions != null) {
                gray = '';
                if (self) {
                    msg = '<span style=float:right;margin-top:1px;margin-right:4px title=' + "Chat" + '><a href=# onclick=userChat(event,"' + encodeURIComponentEx(user._id) + '","' + encodeURIComponentEx(user.name) + '")><img src=\'images/icon-chat.png\' height=16 width=16 style=padding-top:2px /></a></span>';
                    msg += '<span style=float:right;margin-top:1px;margin-left:4px;margin-right:4px title=Notify><a href=# onclick=\'return showUserAlertDialog(event,"' + encodeURIComponentEx(user._id) + '")\'><img src=\'images/icon-notify.png\' height=16 width=16 style=padding-top:2px /></a></span>';
                }
                if (sessions == 1) { lastAccess += nobreak("1 sesión"); } else { lastAccess += nobreak(format("{0} sesiones", sessions)); }
            } else {
                if (user.access) { lastAccess += '<span title="' + format("Último acceso: {0}", printDateTime(new Date(user.access * 1000))) + '">' + printDate(new Date(user.access * 1000)) + '</span>'; }
                else if (user.login) { lastAccess += '<span title="' + format("Último ingreso: {0}", printDateTime(new Date(user.login * 1000))) + '">' + printDate(new Date(user.login * 1000)) + '</span>'; }
            }
            if (self) { permissions += '<a href=# style=cursor:pointer onclick=\'return showUserAdminDialog(event,"' + encodeURIComponentEx(user._id) + '")\'>'; }
            if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) { permissions += "Bloqueada" + ',&nbsp;'; }
            permissions += '<span title=\'' + "Permisos en el Servidor" + '\'>';

            var urights = user.siteadmin & (0xFFFFFFFF - 1248);
            if ((user.siteadmin == null) || (urights == 0)) {
                permissions += "Usuario";
            } else if (urights == 8) {
                permissions += "Usuario + Archivos";
            } else if (user.siteadmin == 0xFFFFFFFF) {
                permissions += "Administrador";
            } else if ((urights & 2) != 0) {
                permissions += "Gestor";
            } else {
                permissions += "Parcial";
            }
            if ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & (64 + 128 + 1024)) != 0)) { permissions += '*'; }
            permissions += '</span>';
            //if ((user.quota != null) && ((user.siteadmin & 8) != 0)) { msg += ", " + (user.quota / 1024) + " k"; }
            if (self) { permissions += '</a>'; }

            var groups = 0
            if (user.links) { for (var i in user.links) { if (i.startsWith('mesh/')) { groups++; } } }

            var username = EscapeHtml(user.name), emailVerified = '';
            if (serverinfo.emailcheck == true) { emailVerified = ((user.emailVerified != true) ? ' <b style=color:red title="' + "Correo electrónico no verificado" + '">&#x2717;</b>' : ' <b style=color:green title="' + "El correo electrónico está verificado" + '">&#x2713</b>'); }
            if (user.email != null) {
                if (((features & 0x200000) == 0) || (user.email.toLowerCase() != user.name.toLowerCase())) {
                    // Username & email are different
                    username += ', <a href="mailto:' + EscapeHtml(user.email) + '">' + EscapeHtml(user.email) + '</a>' + emailVerified;
                } else {
                    // Username & email are the same
                    username += ' <a href="mailto:' + EscapeHtml(user.email) + '"><img src="images/mail12.png" height=9 width=12 title="' + "Enviar correo electrónico al usuario" + '" style="margin-top:2px" /></a>' + emailVerified;
                }
            }

            // If we are a cross-domain administrator, add the domain.
            if ((serverinfo.crossDomain != null)) {
                var userdomain = user._id.split('/')[1];
                if (userdomain != '') { username += ', <span style=color:#26F>' + userdomain + '</span>'; }
            }

            if ((user.otpsecret > 0) || (user.otphkeys > 0) || ((user.otpekey == 1) && (features & 0x00800000)) || (user.otpduo == 1) || ((user.phone != null) && (features & 0x04000000))) { username += ' <i class="fa-solid fa-key" title="' + "Segundo factor de autenticación habilitado" + '" style="margin-top:2px"></i>'; }
            if (user.phone != null) { username += ' <i class="fa-solid fa-mobile-screen" title="' + "Número de teléfono verificado" + '" style="margin-top:2px"></i>'; }
            if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) { username += ' <img src="images/padlock12.png" height=12 width=8 title="' + "La cuenta esta bloqueada" + '" style="margin-top:2px" />'; }
            if ((user.msghandle != null) && (features2 & 0x02000000)) { username += ' <i class="fa-solid fa-comment fa-xs" title="' + "Cuenta de mensajes Verificada" + '" style="margin-top:2px" />'; }
            x += '<tr tabindex=0 onkeypress="if (event.key==\'Enter\') gotoUser(\'' + encodeURIComponentEx(user._id) + '\')"><td>';
            x += '<div>';
            x += '<div class=baricon><input class="UserCheckbox form-check-input me-2" value=' + encodeURIComponentEx(user._id) + ' onclick=p3updateInfo() type=checkbox' + ((user._id == userinfo._id) ? ' disabled' : '') + '></div><div style=cursor:pointer onclick=gotoUser("' + encodeURIComponentEx(user._id) + '",false,event)>';
            x += '<div class=baricon><i class="fa-fw fa-solid fa-user ' + gray + '"></i></div>';
            x += '<div>';
            x += '<div><span style=line-height:24px>' + username + '</span>' + msg + '</div></div><td style=text-align:center>' + groups + '<td style=text-align:center>' + lastAccess + '<td style=text-align:center>' + permissions;
            return x;
        }

        // Called when a user checkbox is clicked
        function p3updateInfo() {
            var elements = document.getElementsByClassName('UserCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) checkcount++; }
            QE('UsersGroupActionButton', checkcount > 0);
            Q('UsersSelectAllButton').value = (checkcount > 0) ? "No seleccionar nada" : "Seleccionar Todo";
        }

        // Called to select all or unselect all users
        function p3usersSelectallButtonFunction() {
            var eself = encodeURIComponentEx(userinfo._id);
            var elements = document.getElementsByClassName('UserCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) checkcount++; }
            for (var i = 0; i < elements.length; i++) { elements[i].checked = (checkcount == 0) && (elements[i].value != eself); }
            p3updateInfo();
        }

        // Called to perform a group action on many users
        function p3usersGroupActionFunction() {
            var elements = document.getElementsByClassName('UserCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) checkcount++; }
            if (checkcount == 0) return;

            var optionalActions = '';
            if (serverinfo.emailcheck) { optionalActions += '<option value=4>' + "Validar Correo Electrónico" + '</option><option value=5>' + "Invalidar Correo Electrónico" + '</option>'; }

            var x = "Selecciona una operación para realizar en todos los usuarios seleccionados." + '<br /><br />';
            x += addHtmlFormFloating("Operación", '<select id=d3groupop class=form-select><option value=1>' + "Bloquear cuenta" + '</option><option value=2>' + "Desbloquear cuenta" + '</option>' + optionalActions + '<option value=3>' + "Eliminar cuenta" + '</option></select>');
            setModalContent('xxAddAgent', "Acción de Grupo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p3usersGroupActionFunctionEx);
        }

        function p3usersGroupActionFunctionEx() {
            var elements = document.getElementsByClassName('UserCheckbox'), userids = [];
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { userids.push(decodeURIComponent(elements[i].value)); } }
            var op = Q('d3groupop').value;
            if (op == 1) {
                // Lock accounts
                for (var i in userids) {
                    var user = users[userids[i]], siteadmin = (user.siteadmin == null) ? 0 : user.siteadmin;
                    if ((siteadmin & 32) == 0) { siteadmin += 32; meshserver.send({ action: 'edituser', id: user._id, siteadmin: siteadmin }); }
                }
            } else if (op == 2) {
                // Unlock accounts
                for (var i in userids) {
                    var user = users[userids[i]], siteadmin = (user.siteadmin == null) ? 0 : user.siteadmin;
                    if ((siteadmin & 32) != 0) { siteadmin -= 32; meshserver.send({ action: 'edituser', id: user._id, siteadmin: siteadmin }); }
                }
            } else if (op == 3) {
                // Delete accounts, ask for confirmation
                var x = "¿Confirmar la eliminación de las cuentas seleccionadas?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p3usersGroupActionFunctionDelCheck() />' + "Confirmar" + '</label>';
                setModalContent('xxAddAgent', "Eliminar Cuentas", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', p3groupActionFunctionDelExec);
                QE('idx_dlgOkButton', false);
                return false;
            } else if (op == 4) {
                // Validate emails
                for (var i in userids) {
                    var user = users[userids[i]];
                    if (user.emailVerified !== true) { meshserver.send({ action: 'edituser', id: user._id, emailVerified: true }); }
                }
            } else if (op == 5) {
                // Invalidate emails
                for (var i in userids) {
                    var user = users[userids[i]];
                    if (user.emailVerified === true) { meshserver.send({ action: 'edituser', id: user._id, emailVerified: false }); }
                }
            }
        }

        function p3usersGroupActionFunctionDelCheck() { QE('idx_dlgOkButton', Q('d3check').checked); }

        // Delete a batch of user accounts
        function p3groupActionFunctionDelExec(b) {
            var elements = document.getElementsByClassName('UserCheckbox'), userids = [];
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { userids.push(decodeURIComponent(elements[i].value)); } }
            for (var i in userids) { var user = users[userids[i]]; meshserver.send({ action: 'deleteuser', userid: user._id, username: user.name }); }
        }

        // Highlights the user being hovered
        function userMouseHover(element, over) {
            var e = element.children[0].children[0].children[1];
            e.children[1].classList.remove('g1s');
            e.children[2].classList.remove('g2s');
            element.children[0].children[0].classList.remove('sbar');
            if (over == 1) {
                e.children[1].classList.add('g1s');
                e.children[2].classList.add('g2s');
                element.children[0].children[0].classList.add('sbar');
            }
        }

        // Highlights the user being hovered
        function userMouseHover2(element, over) {
            var e = element.children[0].children[0];
            e.children[2].classList.remove('g1s');
            e.children[3].classList.remove('g2s');
            element.children[0].children[0].classList.remove('sbar');
            if (over == 1) {
                e.children[2].classList.add('g1s');
                e.children[3].classList.add('g2s');
                element.children[0].children[0].classList.add('sbar');
            }
        }

        function userChat(e, userid, name) {
            if (xxdialogMode) return;
            haltEvent(e);
            var url = '/messenger?id=meshmessenger/' + userid + '/' + encodeURIComponentEx(userinfo._id) + '&title=' + name;
            if ((authCookie != null) && (authCookie != '')) { url += '&auth=' + authCookie; }
            if (urlargs.key) { url += '&key=' + urlargs.key; }
            safeNewWindow(url, 'meshmessenger:' + userid);
            meshserver.send({ action: 'meshmessenger', userid: decodeURIComponent(userid) });
            return false;
        }

        function altUserChat(e, userid, name, i) {
            if (xxdialogMode) return;
            haltEvent(e);
            var url = serverinfo.altmessenging[i].url;
            var ruserid = decodeURIComponent(userid);
            var userid1 = encodeURIComponentEx(ruserid.split('/')[2]); // userid
            var userid2 = encodeURIComponentEx(ruserid.split('/').join('-')); // user-domain-userid
            var userid3 = userid1, userid4 = userid2;
            var ruser = users[ruserid];
            if ((ruser != null) && (ruser.realname != null)) {
                userid3 = encodeURIComponentEx(ruser.realname.split(' ').join('')); // real name with no empty spaces
                userid4 = encodeURIComponentEx(ruser.realname.split(' ').join('-')); // real name with - instead of spaces
            }
            url = url.split('{0}').join(userid1).split('{1}').join(userid2).split('{2}').join(userid3).split('{3}').join(userid4);
            if (urlargs.key) { url += '&key=' + urlargs.key; }
            safeNewWindow(url, 'altmessenger:' + userid);
            meshserver.send({ action: 'notifyuser', userid: decodeURIComponent(userid), msg: serverinfo.altmessenging[i].name, msgid: 11, url: url });
            return false;
        }

        function showSendSMS(userid) {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', "Enviar SMS", '<textarea id=d2smsText maxlength=160 style=width:100%;height:100px;resize:none onKeyUp=showSendSMSValidate()></textarea><span style=font-size:10px><span>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showSendSMSEx(3, userid));
            Q('d2smsText').focus();
            showSendSMSValidate();
        }

        function showSendSMSValidate() { QE('idx_dlgOkButton', Q('d2smsText').value.length > 0); }
        function showSendSMSEx(b, tag) { if (Q('d2smsText').value.length > 0) { meshserver.send({ action: 'smsuser', userid: decodeURIComponent(tag), msg: Q('d2smsText').value }); } }

        function showSendMessage(userid) {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', "Enviar Mensaje", '<textarea id=d2smsText maxlength=160 style=width:100%;height:100px;resize:none onKeyUp=showSendSMSValidate()></textarea><span style=font-size:10px><span>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showSendMessageEx(3, userid));

            Q('d2smsText').focus();
            showSendSMSValidate();
        }

        function showSendMessageEx(b, tag) { if (Q('d2smsText').value.length > 0) { meshserver.send({ action: 'msguser', userid: decodeURIComponent(tag), msg: Q('d2smsText').value }); } }

        function showSendEmail(userid) {
            if (xxdialogMode) return;
            var x = '<input id=d2emailSubject style=width:100% placeholder="' + "Asunto" + '"></input>';
            x += '<textarea id=d2emailText maxlength=10000 style="width:100%;height:200px;resize:none;overflow-y:auto" onKeyUp=showSendEmailValidate()></textarea>';

            setModalContent('xxAddAgent', "Enviar Correo Electrónico", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showSendEmailEx(3, userid));

            Q('d2emailSubject').focus();
            showSendEmailValidate();
        }

        function showSendEmailValidate() { QE('idx_dlgOkButton', (Q('d2emailSubject').value.length > 0) && (Q('d2emailText').value.length > 0)); }
        function showSendEmailEx(b, tag) { if (Q('d2emailText').value.length > 0) { meshserver.send({ action: 'emailuser', userid: decodeURIComponent(tag), subject: Q('d2emailSubject').value, msg: Q('d2emailText').value }); } }

        function showUserAlertDialog(e, userid) {
            if (xxdialogMode) return;
            haltEvent(e);
            var x = '<div style=margin-bottom:6px>' + "Enviar un mensaje de texto a este usuario." + '</div><textarea id=d2notifyText maxlength=2048 style="width:100%;height:184px;resize:none"></textarea>';
            x += '<select style=width:100% id=broadcastMessageMaxTime>';
            x += '<option value=0>' + "Mostrar mensaje hasta que el usuario lo descarte" + '</option>';
            x += '<option value=10>' + "Mostrar por 10 segundos" + '</option>';
            x += '<option value=60>' + "Mostrar por 1 minuto" + '</option>';
            x += '<option value=300>' + "Mostrar por 5 minutos" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', format("Notificar  {0}", EscapeHtml(users[decodeURIComponent(userid)].name)), x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => showUserAlertDialogEx(3, userid));
            Q('d2notifyText').focus();
            return false;
        }

        function showUserAlertDialogEx(button, userid) {
            meshserver.send({ action: 'notifyuser', userid: decodeURIComponent(userid), msg: Q('d2notifyText').value, maxtime: parseInt(Q('broadcastMessageMaxTime').value) });
        }

        function onUsersViewSettings() {
            if (xxdialogMode) return;

            // Use defaults if needed
            if (userViewSettings == null) { userViewSettings = {}; }

            // Display the dialog box
            var x = '';
            x += '<label><input id=d2c1 type=checkbox class="form-check-input me-2"' + (userViewSettings.noViewLimit ? '' : ' checked') + '>' + "Mostrar solo los primeros 100 usuarios" + '</label><br />';
            setModalContent('xxAddAgent', "Vista de usuarios", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', onUsersViewSettingsEx);
        }

        function onUsersViewSettingsEx() {
            userViewSettings.noViewLimit = !Q('d2c1').checked;
            putstore('_usersViewSettings', JSON.stringify(userViewSettings));
            mainUpdate(16384); // Update users
        }

        function p4batchAccountCreate() {
            if (xxdialogMode) return;
            var x = "Create many accounts at once by importing a JSON or CSV file" + '<br /><br />';
            x += "JSON file format is as follows:" + '<br />';
            x += '<pre>[\r\n {"user":"x1","pass":"x","email":"x1@x"},\r\n {"user":"x2","pass":"x","resetNextLogin":true}\r\n]</pre><br />';
            x += "CSV file format is as follows:" + '<br />';
            x += '<pre>user,pass,email,resetNextLogin\r\nx1,x,x1@x,\r\nx2,x,,true\r\n</pre><br />';
            x += '<input type=file id=d4importFile class="form-control" accept=".json,.csv" onchange=p4batchAccountCreateValidate() />';
            setModalContent('xxAddAgent', "Importar Cuenta de Usuario", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p4batchAccountCreateEx);
            //QE('idx_dlgOkButton', false);
        }

        function p4batchAccountCreateValidate() {
            QE('idx_dlgOkButton', Q('d4importFile').value != null);
        }

        function p4batchAccountCreateEx() {
            var fr = new FileReader();
            fr.onload = function (r) {
                var j = null;
                if (Q('d4importFile').value.endsWith('.csv')) {
                    const csvData = r.target.result;
                    const lines = csvData.split('\n');
                    const headers = lines[0].trim().split(',');
                    const jsonArray = [];
                    for (let i = 1; i < lines.length; i++) {
                        const currentLine = lines[i].trim();
                        if (currentLine === "") continue; // Skip blank lines
                        const lineArray = currentLine.split(',');
                        // Check if any non-empty field exists
                        let hasNonEmptyField = false;
                        for (let j = 0; j < lineArray.length; j++) {
                            if (lineArray[j].trim() !== "") {
                                hasNonEmptyField = true;
                                break;
                            }
                        }
                        // If no non-empty field exists, skip adding this object
                        if (!hasNonEmptyField) continue;
                        const obj = {};
                        for (let j = 0; j < headers.length; j++) {
                            // Only include columns with non-empty headers and non-empty values
                            const value = lineArray[j].trim();
                            if (headers[j].trim() !== "" && value !== "") {
                                // Convert values "true" to true
                                obj[headers[j]] = value.toLowerCase() === 'true' ? true : value;
                            }
                        }
                        jsonArray.push(obj);
                    }
                    j = jsonArray;
                    if ((j != null) && (Array.isArray(j))) {
                        var ok = true;
                        for (var i in j) {
                            if ((typeof j[i].user != 'string') || (j[i].user.length < 1) || (j[i].user.length > 64)) { ok = false; }
                            if ((typeof j[i].pass != 'string') || (j[i].pass.length < 1) || (j[i].pass.length > 256)) { ok = false; }
                            if (checkPasswordRequirements(j[i].pass, passRequirements) == false) { ok = false; }
                            if ((j[i].email != null) && ((typeof j[i].email != 'string') || (j[i].email.length < 1) || (j[i].email.length > 128))) { ok = false; }
                        }
                        if (ok == false) {
                            setModalContent('xxAddAgent', "Importar Cuenta de Usuario", "Invalid CSV file format.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Importar Cuenta de Usuario", "Invalid CSV file format.");
                    }
                } else {
                    try {
                        j = JSON.parse(r.target.result);
                    } catch (ex) {
                        setModalContent('xxAddAgent', "Importar Cuenta de Usuario", format("Archivo JSON inválido: {0}.", ex));
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        return;
                    }
                    if ((j != null) && (Array.isArray(j))) {
                        var ok = true;
                        for (var i in j) {
                            if ((typeof j[i].user != 'string') || (j[i].user.length < 1) || (j[i].user.length > 64)) { ok = false; }
                            if ((typeof j[i].pass != 'string') || (j[i].pass.length < 1) || (j[i].pass.length > 256)) { ok = false; }
                            if (checkPasswordRequirements(j[i].pass, passRequirements) == false) { ok = false; }
                            if ((j[i].email != null) && ((typeof j[i].email != 'string') || (j[i].email.length < 1) || (j[i].email.length > 128))) { ok = false; }
                        }
                        if (ok == false) {
                            setModalContent('xxAddAgent', "Importar Cuenta de Usuario", "Formato de Archivo JSON no válido.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Importar Cuenta de Usuario", "Formato de Archivo JSON no válido.");
                    }
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            };
            fr.readAsText(Q('d4importFile').files[0]);
        }

        function p4downloadUserInfo() {
            if (xxdialogMode) return;
            var x = "Descargar la lista de usuarios con uno de los formatos de archivo a continuación." + '<br /><br />';
            x += addHtmlValue("Formato CSV", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoCSV()\'>' + 'userlist.csv' + '</a>');
            x += addHtmlValue("Formato JSON", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoJSON()\'>' + 'userlist.json' + '</a>');
            setModalContent('xxAddAgent', "Exportar Lista de Usuarios", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

        function p4downloadUserInfoCSV() {
            var csv = "id,nombre,correo electrónico,creación,último inicio de sesión,grupos,factores de autenticación,administrador del sitio,administrador de usuarios,bloqueado" + '\r\n';
            for (var i in users) {
                var multiFactor = false, factors = [];
                if ((users[i].otpsecret > 0) || (users[i].otphkeys > 0)) {
                    multiFactor = true;
                    if (users[i].otpsecret > 0) { factors.push('AuthApp'); }
                    if (users[i].otphkeys > 0) { factors.push('SecurityKey'); }
                    if (users[i].otpkeys > 0) { factors.push('BackupCodes'); }
                }
                csv += '"' + users[i]._id + '","' + users[i].name + '","' + (users[i].email ? users[i].email : '') + '","' + (users[i].creation ? new Date(users[i].creation * 1000) : '') + '","' + (users[i].login ? new Date(users[i].login * 1000) : '') + '","' + (users[i].groups ? users[i].groups.join(',') : '') + '","' + ((multiFactor ? factors.join(',') : '') + '"');
                csv += ',' + ((users[i].siteadmin == 0xFFFFFFFF) ? '1' : '0'); // site admin
                csv += ',' + (((users[i].siteadmin & 2) != 0) ? '1' : '0'); // user admin
                csv += ',' + (((users[i].siteadmin & 32) != 0) ? '1' : '0'); // locked
                csv += '\r\n';
            }
            saveAs(stringToUtf8Blob(csv), 'userlist.csv');
            return false;
        }

        function p4downloadUserInfoJSON() {
            var r = []
            for (var i in users) { r.push(users[i]); }
            saveAs(new Blob([JSON.stringify(r, null, 2)], { type: 'application/octet-stream' }), 'userlist.json');
            return false;
        }

        function showUserBroadcastDialog(targetid) {
            if (xxdialogMode) return;
            var x = '<div style=margin-bottom:6px>' + "Enviar un mensaje a todos los usuarios conectados." + '</div><textarea id=broadcastMessage class="form-control mb-2" style="width:100%;height:184px;resize:none" value="" maxlength=2048 /></textarea>';
            x += '<select style=width:100% class="form-select" id=broadcastMessageMaxTime>';
            x += '<option value=0>' + "Mostrar mensaje hasta que el usuario lo descarte" + '</option>';
            x += '<option value=10>' + "Mostrar por 10 segundos" + '</option>';
            x += '<option value=60>' + "Mostrar por 1 minuto" + '</option>';
            x += '<option value=300>' + "Mostrar por 5 minutos" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', "Mensaje de Broadcast", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                showUserBroadcastDialogEx(3, targetid ? decodeURIComponent(targetid) : null);
            });
            Q('broadcastMessage').focus();
        }

        function showUserBroadcastDialogEx(b, targetid) {
            meshserver.send({ action: 'userbroadcast', msg: Q('broadcastMessage').value, target: targetid, maxtime: parseInt(Q('broadcastMessageMaxTime').value) });
        }

        function showCreateNewAccountDialog() {
            if (xxdialogMode) return;
            var x = '';

            if (serverinfo.crossDomain) {
                var y = '<select class="form-select" id=p4domain>';
                for (var i in serverinfo.crossDomain) { y += '<option value=' + i + '>' + ((serverinfo.crossDomain[i] == '') ? "Por Defecto" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Dominio", y);
            }

            if ((features & 0x200000) == 0) { x += addHtmlFormFloating('<span id=p4hname>' + "Nombre de Usuario" + '</span>', '<input id=p4name class="form-control" maxlength=64 autocomplete=username onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />'); }
            x += addHtmlFormFloating('<span id=p4hemail>' + "Correo electrónico", '<input id=p4email type=email class="form-control" maxlength=256 autocomplete="email" inputmode="email" onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />');
            x += '';
            x += addHtmlFormFloating('<span id=p4hp1>' + "Contraseña" + '</span>', '<input id=p4pass1 type=password class="form-control" maxlength=256 autocomplete="new-password" onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />');
            x += addHtmlFormFloating('<span id=p4hp2>' + "Confirm Password" + '</span>', '<input id=p4pass2 type=password class="form-control" maxlength=256 autocomplete="new-password" onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />');
            x += '<div><label><input id=p4randomPassword onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Hacer Contraseña Aleatoria." + '</label></div>';
            x += '<div><label><input id=p4removeEvents onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Remover todos los eventos anteriores para este ID de usuario." + '</label></div>';
            x += '<div><label><input id=p4resetNextLogin onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Forzar restablecimiento de contraseña en el próximo inicio de sesión." + '</label></div>';
            if (serverinfo.emailcheck) {
                x += '<div><label><input id=p4verifiedEmail onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Correo electrónico verificado." + '</label></div>';
                x += '<div><label><input id=p4invitationEmail type=checkbox class="form-check-input me-2" title=' + "Correo electrónico verificado y restablecimiento de contraseña forzado requerido." + ' />' + "Enviar correo electrónico de invitación." + '</label></div>';
            }

            if (passRequirements) {
                var r = [], rc = 0;
                for (var i in passRequirements) { if ((i != 'reset') && (i != 'hint')) { r.push(i + ':' + passRequirements[i]); rc++; } }
                if (rc > 0) { x += '<div style=font-size:x-small;padding:6px>' + format("Requerimientos: {0}.", r.join(', ')) + '</div>'; }
            }

            setModalContent('xxAddAgent', "Crear Cuenta", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', showCreateNewAccountDialogEx);
            showCreateNewAccountDialogValidate();
            if ((features & 0x200000) == 0) { Q('p4name').focus(); } else { Q('p4email').focus(); }
        }

        function showCreateNewAccountDialogValidate() {
            var emailok = validateEmail(Q('p4email').value);
            var nameok = true;
            var passok = (Q('p4pass1').value.length > 0 && Q('p4pass1').value == Q('p4pass2').value && checkPasswordRequirements(Q('p4pass1').value, passRequirements));

            if ((features & 0x200000) == 0) {
                nameok = (!Q('p4name') || ((Q('p4name').value.length > 0) && (Q('p4name').value.indexOf(' ') == -1)));
                QS('p4hname').color = nameok ? 'black' : '#7b241c';
            }
            QS('p4hemail').color = emailok ? 'black' : '#7b241c';

            if (serverinfo.emailcheck) {
                QE('p4verifiedEmail', emailok);
                QE('p4invitationEmail', emailok && Q('p4resetNextLogin').checked && Q('p4verifiedEmail').checked);
                if (emailok == false) { Q('p4verifiedEmail').checked = false; }
                if ((Q('p4resetNextLogin').checked == false) || (Q('p4verifiedEmail').checked == false)) { Q('p4invitationEmail').checked = false; }
            }
            QE('p4pass1', !Q('p4randomPassword').checked);
            QE('p4pass2', !Q('p4randomPassword').checked);
            QS('p4hp1').color = (passok || Q('p4randomPassword').checked) ? 'black' : '#7b241c';
            QS('p4hp2').color = (passok || Q('p4randomPassword').checked) ? 'black' : '#7b241c';

            var ok = nameok & emailok;
            if (Q('p4randomPassword').checked == false) { ok &= passok; }
            QE('idx_dlgOkButton', ok);
        }

        function showCreateNewAccountDialogEx() {
            var username = ((features & 0x200000) == 0) ? Q('p4name').value : Q('p4email').value; // Username is email address
            var x = { action: 'adduser', username: username, email: Q('p4email').value, pass: Q('p4pass1').value, resetNextLogin: Q('p4resetNextLogin').checked, randomPassword: Q('p4randomPassword').checked, removeEvents: Q('p4removeEvents').checked };
            if (serverinfo.emailcheck) {
                x.emailVerified = Q('p4verifiedEmail').checked;
                x.emailInvitation = Q('p4invitationEmail').checked;
            }
            if (serverinfo.crossDomain) { x.domain = serverinfo.crossDomain[parseInt(Q('p4domain').value)]; }
            meshserver.send(x);
        }

        function showUserGroupDialog(e, userid) {
            if (xxdialogMode) return;
            haltEvent(e);
            userid = decodeURIComponent(userid);
            var user = users[userid.toLowerCase()], groups = "";
            if (user.groups != null) { groups = user.groups.join(', ') }
            var x = "Ingresa una lista separada por comas de nombres de reinos administrativos." + '<br /><br />';
            x += `<div class="form-floating mb-2"><input id=dp4usergroups value="${groups}" class="form-control" placeholder="Name1, Name2, Name3" maxlength=256 onchange=p4validateUserGroups() onkeyup=p4validateUserGroups() /><label for=dp4usergroups>Realms</label></div>`;
            setModalContent('xxAddAgent', "Reinos Administrativos", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                showUserGroupDialogEx(e, user);
            });
            focusTextBox('dp4usergroups');
            p4validateUserGroups();
            return false;
        }

        function p4validateUserGroups() {
            var groups = Q('dp4usergroups').value;
            var k = 0, i = groups.indexOf('"') + groups.indexOf('/') + groups.indexOf('>') + groups.indexOf('<') + groups.indexOf('\'');
            var g = groups.split(',');
            for (var j in g) { if (g[j].trim().length == 0) k++; }
            QE('idx_dlgOkButton', (groups == '') || ((i == -5) && (k < 1)));
        }

        function showUserGroupDialogEx(event, user) {
            var groups = Q('dp4usergroups').value, g = groups.split(','), g2 = [];
            for (var j in g) { var x = g[j].trim(); if (x.length > 0) { g2.push(x); } }
            meshserver.send({ action: 'edituser', id: user._id, groups: g2 });
        }

        function showUserAdminDialog(e, userid) {
            if (xxdialogMode) return;
            haltEvent(e);
            userid = decodeURIComponent(userid);
            var user = users[userid];
            if (user == null) return;
            var uself = (userinfo._id == user._id);
            var x = '';
            x += '<div class="d-inline-flex align-items-center"><label class="form-label me-2"><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_fileaccess>' + "Archivos del Servidor" + '</label>, <input type=number class="me-1 form-control" onchange=showUserAdminDialogValidate() maxlength=10 id=ua_fileaccessquota>' + "k máximo, en blanco por defecto" + '</div><br><hr/>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_fulladmin>' + "Administrador Total" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverbackup>' + "Realizar Copias de Seguridad del Servidor" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverrestore>' + "Restaurar el Servidor con una Copia de Seguridad" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverupdate>' + "Realizar Actualizaciones del Servidor" + '</label><br></div>';
            x += '<div id=d2MngUsr><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusers>' + "Administrar Usuarios" + '</label><br></div>';
            x += '<div id=d2MngGrp><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusergroups>' + "Administrar Grupos de Usuarios" + '</label><br></div>';
            x += '<div id=d2MngRec><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_managerecordings>' + "Administrar Grabaciones" + '</label><br></div>';
            x += '<div id=d2AllEvnt><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_allevents>' + "Ver Todos los Eventos" + '</label><br></div>';
            if (x != '') { x += '<hr/>'; }
            x = '<div><div id=d2AdminPermissions>' + x;
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_lockedaccount>' + "Bloquear Cuenta" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewgroups>' + "No hay Nuevos Groups de Dispositivos" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewdevices>' + "No hay nuevos dispositivos" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nomeshcmd>' + "Sin Herramientas (MeshCmd/Router)" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_locksettings>' + "Bloquear Opciones de la Cuenta" + '</label><br>';
            x += '</div>';

            setModalContent('xxAddAgent', "Permisos en el Servidor", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                showUserAdminDialogEx(2 + (uself ? 0 : 1), user);
            });

            if (user.siteadmin && user.siteadmin != 0) {
                Q('ua_fulladmin').checked = (user.siteadmin == 0xFFFFFFFF);
                Q('ua_serverbackup').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1) != 0));       // Server Backup
                Q('ua_serverrestore').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 4) != 0));      // Server Restore
                Q('ua_fileaccess').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 8) != 0));         // Server Files
                Q('ua_serverupdate').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 16) != 0));      // Server Update
                Q('ua_lockedaccount').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 32) != 0));     // Account locked
                Q('ua_nonewgroups').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 64) != 0));       // No New Groups
                Q('ua_nomeshcmd').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 128) != 0));        // No Tools (MeshCMD / Router)
                Q('ua_locksettings').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0));    // Lock account settings
                Q('ua_nonewdevices').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 4096) != 0));    // No New Devices
            }
            if ((userinfo.siteadmin & 2) != 0) {
                Q('ua_manageusers').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 2) != 0));        // Manage Users
                QE('ua_manageusers', !uself && (userinfo.siteadmin & 2));
            }
            if ((userinfo.siteadmin & 256) != 0) {
                Q('ua_manageusergroups').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 256) != 0)); // Manage User Groups
                QE('ua_manageusergroups', !uself && (userinfo.siteadmin & 256));
            }
            if ((userinfo.siteadmin & 512) != 0) {
                Q('ua_managerecordings').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 512) != 0)); // Manage Recordings
                QE('ua_managerecordings', !uself && (userinfo.siteadmin & 512));
            }
            if ((userinfo.siteadmin & 2048) != 0) {
                Q('ua_allevents').checked = ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 2048) != 0));       // View all events
                QE('ua_allevents', !uself && (userinfo.siteadmin & 2048));
            }

            QE('ua_fulladmin', !uself && (userinfo.siteadmin == 0xFFFFFFFF));
            QE('ua_serverbackup', !uself && (userinfo.siteadmin == 0xFFFFFFFF));
            QE('ua_serverrestore', !uself && (userinfo.siteadmin == 0xFFFFFFFF));
            QE('ua_fileaccess', !uself && (userinfo.siteadmin == 0xFFFFFFFF));
            QE('ua_fileaccessquota', !uself && (userinfo.siteadmin == 0xFFFFFFFF));
            QE('ua_serverupdate', !uself && (userinfo.siteadmin == 0xFFFFFFFF));
            QV('d2AdminPermissions', userinfo.siteadmin == 0xFFFFFFFF)

            QV('d2MngUsr', (userinfo.siteadmin & 2) != 0);
            QV('d2MngGrp', (userinfo.siteadmin & 256) != 0);
            QV('d2MngRec', (userinfo.siteadmin & 512) != 0);

            QE('ua_lockedaccount', !uself && (userinfo.siteadmin & 2) && (user.siteadmin != 0xFFFFFFFF) && (userinfo._id != user._id));
            QE('ua_nonewgroups', !uself && (userinfo.siteadmin & 2) && (user.siteadmin != 0xFFFFFFFF) && (userinfo._id != user._id));
            QE('ua_nomeshcmd', !uself && (userinfo.siteadmin & 2) && (user.siteadmin != 0xFFFFFFFF) && (userinfo._id != user._id));
            QE('ua_nonewdevices', !uself && (userinfo.siteadmin & 2) && (user.siteadmin != 0xFFFFFFFF) && (userinfo._id != user._id));
            QE('ua_locksettings', !uself && (userinfo.siteadmin & 2) && (user.siteadmin != 0xFFFFFFFF) && (userinfo._id != user._id));
            Q('ua_fileaccessquota').value = (user.quota != null) ? (user.quota / 1024) : '';
            showUserAdminDialogValidate();
            return false;
        }

        function showUserAdminDialogValidate() {
            if (userinfo.siteadmin == 0xFFFFFFFF) {
                QE('ua_serverbackup', !Q('ua_fulladmin').checked);
                QE('ua_manageusers', !Q('ua_fulladmin').checked);
                QE('ua_serverrestore', !Q('ua_fulladmin').checked);
                QE('ua_fileaccess', !Q('ua_fulladmin').checked);
                QE('ua_serverupdate', !Q('ua_fulladmin').checked);
                QE('ua_lockedaccount', !Q('ua_fulladmin').checked);
                QE('ua_nonewgroups', !Q('ua_fulladmin').checked);
                QE('ua_nomeshcmd', !Q('ua_fulladmin').checked);
                QE('ua_nonewdevices', !Q('ua_fulladmin').checked);
                QE('ua_manageusergroups', !Q('ua_fulladmin').checked);
                QE('ua_managerecordings', !Q('ua_fulladmin').checked);
                QE('ua_allevents', !Q('ua_fulladmin').checked);
                QE('ua_locksettings', !Q('ua_fulladmin').checked);
                QE('ua_fileaccessquota', Q('ua_fileaccess').checked && !Q('ua_fulladmin').checked);
            }
        }

        function showUserAdminDialogEx(button, user) {
            var siteadmin = 0, quota = parseInt(Q('ua_fileaccessquota').value);
            if (Q('ua_fulladmin').checked == true) { siteadmin = 0xFFFFFFFF; } else {
                if (Q('ua_serverbackup').checked == true) siteadmin += 1;
                if (Q('ua_manageusers').checked == true) siteadmin += 2;
                if (Q('ua_serverrestore').checked == true) siteadmin += 4;
                if (Q('ua_fileaccess').checked == true) siteadmin += 8;
                if (Q('ua_serverupdate').checked == true) siteadmin += 16;
                if (Q('ua_lockedaccount').checked == true) siteadmin += 32;
                if (Q('ua_nonewgroups').checked == true) siteadmin += 64;
                if (Q('ua_nomeshcmd').checked == true) siteadmin += 128;
                if (Q('ua_manageusergroups').checked == true) siteadmin += 256;
                if (Q('ua_managerecordings').checked == true) siteadmin += 512;
                if (Q('ua_locksettings').checked == true) siteadmin += 1024;
                if (Q('ua_allevents').checked == true) siteadmin += 2048;
                if (Q('ua_nonewdevices').checked == true) siteadmin += 4096;
            }
            var x = { action: 'edituser', id: user._id, siteadmin: siteadmin };
            if (isNaN(quota) == false) { x.quota = (quota * 1024); }
            meshserver.send(x);
        }

        function onUserSearchInputChanged() { mainUpdate(16384); }


        //
        // MY USER GROUPS
        //

        function updateUserGroups() {
            // Display user group operations only if allowed for us
            QV('p50userGroupOps', (userinfo.siteadmin & 256) != 0); // SITERIGHT_USERGROUPS = 256

            // Sort the list of group names
            var sortedGroups = [], x = '';
            if (usergroups) { for (var i in usergroups) { sortedGroups.push(usergroups[i]); } }
            sortedGroups.sort(nameSort);

            // Save the list of currently checked users
            var checkedUserGroupids = [], elements = document.getElementsByClassName('UserGroupCheckbox');
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked) { checkedUserGroupids.push(elements[i].value); } }

            if (sortedGroups.length == 0) {
                x += '<br />' + "No se encontraron grupos." + '<br />';
                QV('DuplicateUserGroupButton', false);
            } else {
                // Display the groups using the sorted list
                x += '<table class="table table-hover p3usersTable" cellpadding=0 cellspacing=0>';
                x += '<th>' + "Nombre" + '<th style=width:80px>' + "Usuarios" + '<th style=width:80px>' + "Grupos de Dispositivos" + '<th style=width:80px>' + "Dispositivos";
                for (var i in sortedGroups) { x += addUserGroupHtml(sortedGroups[i]); }
                x += '</table>';
                QV('DuplicateUserGroupButton', true);
            }
            QH('p50groups', x);

            // Re-check userid's
            elements = document.getElementsByClassName('UserGroupCheckbox');
            for (var i = 0; i < elements.length; i++) { elements[i].checked = ((checkedUserGroupids.indexOf(elements[i].value) >= 0)); }
            p50updateInfo();

            // Update current user panel if needed
            if ((currentUserGroup != null) && (xxcurrentView == 51)) { gotoUserGroup(encodeURIComponentEx(currentUserGroup._id), true); }
        }

        function addUserGroupHtml(group) {
            var usercount = 0, meshcount = 0, devicecount = 0;
            if (group.links) { for (var i in group.links) { if (i.startsWith('user/')) { usercount++; } if (i.startsWith('mesh/')) { meshcount++; } if (i.startsWith('node/')) { devicecount++; } } }

            // Group name, if we are a cross-domain administrator, add the domain.
            var name = EscapeHtml(group.name);
            if ((serverinfo.crossDomain != null)) {
                var grpdomain = group._id.split('/')[1];
                if (grpdomain != '') { name += ', <span style=color:#26F>' + EscapeHtml(grpdomain) + '</span>'; }
            }

            var x = '<tr tabindex=0 onkeypress="if (event.key==\'Enter\') gotoUserGroup(\'' + encodeURIComponentEx(group._id) + '\')"><td style=cursor:pointer>';
            x += '<div style=width:100%>';
            x += '<div class=baricon><input class="form-check-input me-2 UserGroupCheckbox" value=' + encodeURIComponentEx(group._id) + ' onclick=p50updateInfo() type=checkbox></div>';
            x += '<div class=baricon onclick=gotoUserGroup("' + encodeURIComponentEx(group._id) + '")><i class="fa-solid fa-users"></i></div>';
            x += '<div onclick=gotoUserGroup("' + encodeURIComponentEx(group._id) + '")></div><div onclick=gotoUserGroup("' + encodeURIComponentEx(group._id) + '")></div>';
            x += '<div style=line-height:24px onclick=gotoUserGroup("' + encodeURIComponentEx(group._id) + '")><span style=font-size:16px>' + name + '</span></div></div><td style=text-align:center>' + usercount + '<td style=text-align:center>' + meshcount + '<td style=text-align:center>' + devicecount;
            return x;
        }

        function p50updateInfo() {
            var elements = document.getElementsByClassName('UserGroupCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) checkcount++; }
            QE('UsersGroupsGroupActionButton', checkcount > 0);
            Q('UsersGroupsSelectAllButton').value = (checkcount > 0) ? "No seleccionar nada" : "Seleccionar Todo";
        }

        // Called to select all or unselect all users
        function p50usersSelectallButtonFunction() {
            var eself = encodeURIComponentEx(userinfo._id);
            var elements = document.getElementsByClassName('UserGroupCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) checkcount++; }
            for (var i = 0; i < elements.length; i++) { elements[i].checked = (checkcount == 0) && (elements[i].value != eself); }
            p50updateInfo();
        }

        // Called to perform a group action on many users
        function p50usersGroupActionFunction() {
            var elements = document.getElementsByClassName('UserGroupCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) checkcount++; }
            if (checkcount == 0) return;
            var x = "Selecciona una operación para realizar en todos los usuarios seleccionados." + '<br /><br />';
            x += addHtmlFormFloating("Operación", '<select id=d50groupop class=form-select><option value=1>' + "Eliminar grupo de usuarios" + '</option></select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Acción de Grupo", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p50usersGroupActionFunctionEx);
        };

        function p50usersGroupActionFunctionEx() {
            var elements = document.getElementsByClassName('UserGroupCheckbox'), userids = [];
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { userids.push(decodeURIComponent(elements[i].value)); } }
            var op = Q('d50groupop').value;
            if (op == 1) {
                // Delete user groups, ask for confirmation
                var x = "¿Confirmar la eliminación de grupos de usuarios seleccionados?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p50usersGroupActionFunctionDelCheck() />' + "Confirmar" + '</label>';
                setModalContent('xxAddAgent', "Eliminar Grupos de Usuarios", x);
                // Set action manually
                document.getElementById('idx_dlgOkButton').onclick = function () {
                    p50groupActionFunctionDelExec(3);
                };
            }
        }

        function p50usersGroupActionFunctionDelCheck() { QE('idx_dlgOkButton', Q('d3check').checked); }

        // Delete a batch of user accounts
        function p50groupActionFunctionDelExec(b) {
            var elements = document.getElementsByClassName('UserGroupCheckbox');
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { meshserver.send({ action: 'deleteusergroup', ugrpid: decodeURIComponent(elements[i].value) }); } }
        }

        function showCreateUserGroupDialog(mode) {
            if (xxdialogMode) return;
            var x = '', y = '';
            if (mode == 2) {
                if (usergroups) { for (var i in usergroups) { y += '<option value=' + encodeURIComponentEx(i) + '>' + EscapeHtml(usergroups[i].name) + '</option>'; } }
                x += addHtmlFormFloating("Grupo de Usuarios", '<select id=dp4groupid class="form-select">' + y + '</select>');
            }
            if ((mode == 1) && (serverinfo.crossDomain)) {
                var y = '<select id=p4domain class="form-select">';
                for (var i in serverinfo.crossDomain) { y += '<option value=' + i + '>' + ((serverinfo.crossDomain[i] == '') ? "Por Defecto" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Dominio", y);
            }
            x += addHtmlFormFloating("Nombre", '<input id=p4name class=form-control maxlength=64 onchange=showCreateUserGroupDialogValidate() onkeyup=showCreateUserGroupDialogValidate() />');
            x += addHtmlFormFloating("Descripción", '<textarea id=p4desc class=form-control value="" maxlength=1024 /></textarea>');
            setModalContent('xxAddAgent', (mode == 1) ? "Crear Grupo de Usuarios" : "Duplicar Grupo de Usuarios", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                showCreateUserGroupDialogEx(3, mode);
            });
            showCreateUserGroupDialogValidate();
            Q('p4name').focus();
        }

        function showCreateUserGroupDialogValidate() { QE('idx_dlgOkButton', Q('p4name').value.length > 0); }

        function showCreateUserGroupDialogEx(b, mode) {
            var x = { action: 'createusergroup', name: Q('p4name').value, desc: Q('p4desc').value };
            if (mode == 2) { x.clone = decodeURIComponent(Q('dp4groupid').value); }
            if ((mode == 1) && (serverinfo.crossDomain)) { x.domain = serverinfo.crossDomain[parseInt(Q('p4domain').value)]; }
            meshserver.send(x);
        }

        //
        // MY USER GROUP
        //

        var currentUserGroup = null;
        function gotoUserGroup(groupid, force) {
            if (xxdialogMode && !force) return;
            var group = currentUserGroup = usergroups ? usergroups[decodeURIComponent(groupid)] : null;
            if (group == null) { if (xxcurrentView == 51) { go(50); } return; }

            // Add user group name
            var gname = EscapeHtml(group.name);
            if (gname.length == 0) { gname = '<i>' + "Ninguno" + '</i>'; }
            if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { gname = '<span role=button tabindex=0 title="' + "Haz clic aquí para editar el nombre del grupo de usuarios" + '" onclick=p51editgroup(1) onkeyup="if (event.key == \'Enter\') p51editgroup(1)">' + gname + '  <i class="fa-solid fa-pencil fa-2xs"/></i></span>'; }
            QH('p51groupName', gname);

            var usercount = 0, meshcount = 0, devicecount = 0;
            if (group.links) {
                for (var i in group.links) {
                    if (i.startsWith('user/')) { usercount++; }
                    if (i.startsWith('mesh/')) { meshcount++; }
                    if (i.startsWith('node/')) { devicecount++; }
                }
            }
            var desc = group.desc;
            if ((desc == null) || (desc == '')) { desc = '<i>' + "Ninguno" + '<i>'; } else { desc = EscapeHtml(desc); }

            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Nombre", gname); } // If title bar is hidden, display the user group name here
            if ((serverinfo.crossDomain != null) || (debugmode != 0)) {
                var d = group._id.split('/')[1];
                x += addDeviceAttribute("Dominio", (d != '') ? EscapeHtml(d) : ('<i>' + "Por Defecto" + '</i>'));
                x += addDeviceAttribute("Identificador del Grupo", EscapeHtml(group._id));
            }
            if (currentUserGroup.membershipType != null) {
                x += addDeviceAttribute("Tipo de grupo", EscapeHtml(currentUserGroup.membershipType));
            }
            if ((userinfo.siteadmin & 256) != 0) {
                x += addDeviceAttribute("Descripción", '<span role=button onclick=p51editgroup(2,' + (currentUserGroup.membershipType != null) + ')>' + desc + ' <i class="fa-solid fa-pencil fa-xs"/></i></span>');
            } else {
                x += addDeviceAttribute("Descripción", desc);
            }

            // Display features
            if (serverinfo.userGroupsSessionRecording == 1) {
                var userGroupFeatures = [];
                if ((group.flags) && (group.flags & 2)) { userGroupFeatures.push("Grabar Sesiones"); }
                userGroupFeatures = userGroupFeatures.join(', ');
                if (userGroupFeatures == '') { userGroupFeatures = '<i>' + "Ninguno" + '</i>'; }
                x += addDeviceAttribute("Caracteristicas", addLink(userGroupFeatures, 'p51edituserGroupFeatures()'));
            }

            // Display user consent flags for this user group
            {
                var consentOptionsStr = [], consent = 0;
                if (group.consent) { consent = group.consent; }
                if (serverinfo.consent) { consent |= serverinfo.consent; }
                if ((consent & 0x0040) && (consent & 0x0008)) { consentOptionsStr.push("Mensaje de Escritorio + Barra de Herramientas"); } else if (consent & 0x0040) { consentOptionsStr.push("Barra de Herramientas de Escritorio"); } else if (consent & 0x0008) { consentOptionsStr.push("Mensaje de Escritorio"); } else { if (consent & 0x0001) { consentOptionsStr.push("Notificación de Escritorio"); } }
                if (consent & 0x0010) { consentOptionsStr.push("Mensaje en Terminal"); } else { if (consent & 0x0002) { consentOptionsStr.push("Notificación de Terminal"); } }
                if (consent & 0x0020) { consentOptionsStr.push("Solicitud de Archivos"); } else { if (consent & 0x0004) { consentOptionsStr.push("Notificación de Archivos"); } }
                if (consent == 7) { consentOptionsStr = ["Siempre Notificar"]; }
                if ((consent & 56) == 56) { consentOptionsStr = ["Siempre Preguntar"]; }

                consentOptionsStr = consentOptionsStr.join(', ');
                if (consentOptionsStr == '') { consentOptionsStr = '<i>' + "Ninguno" + '</i>'; }
                x += addDeviceAttribute("Consentimiento del Usuario", addLinkConditional(consentOptionsStr, 'p20editmeshconsent(4)', true));
            }

            x += addDeviceAttribute("Usuarios", usercount);
            x += addDeviceAttribute("Grupos de Dispositivos", meshcount);
            x += addDeviceAttribute("Dispositivos", devicecount);

            x += '</table></div><br />';

            if ((userinfo.siteadmin & 256) != 0) {
                x += '<input type=button class="btn btn-primary btn-sm" value="' + "Broadcast" + '" title="' + "Enviar un mensaje a todos los usuarios de este grupo." + '" onclick=showUserBroadcastDialog("' + encodeURIComponentEx(group._id) + '") />';
            }

            // Setup the panel
            QH('p51group', x);

            x = '<br />';
            if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) {
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick="return p51showAddUserDialog()"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Usuarios" + '</button>';
            }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Miembros del Grupo" + '</th><th scope=col></th></tr>';

            // Sort the users for this mesh
            var count = 1, sortedusers = [];
            for (var i in currentUserGroup.links) {
                if (i.startsWith('user/') == false) continue;
                var uname = i.split('/')[2];
                if (currentUserGroup.links[i].name) { uname = currentUserGroup.links[i].name; }
                if (i == userinfo._id) { uname = userinfo.name; }
                sortedusers.push({ id: i, name: uname, rights: currentUserGroup.links[i].rights });
            }
            sortedusers.sort(function (a, b) { if (a.name > b.name) return 1; if (a.name < b.name) return -1; return 0; });

            // Display all users for this user group
            for (var i in sortedusers) {
                var trash = '';
                if (currentUserGroup.membershipType == null) { trash = '<a href=# onclick=\'return p51deleteUser(event,"' + encodeURIComponentEx(sortedusers[i].id) + '")\' title="' + "Remover permisos de usuario de este grupo de dispositivos" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>'; }
                var username = EscapeHtml(decodeURIComponent(sortedusers[i].name));
                if (users != null) { username = '<a href=# onclick=\'gotoUser("' + encodeURIComponentEx(sortedusers[i].id) + '");haltEvent(event);\'>' + username + '</a>'; }
                x += '<tr ' + (((count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td><i class="fa-solid fa-user fa-fw" title="' + "Usuario" + '"></i>&nbsp;' + username + '</td><td><div style=float:right>' + trash + '</div></td></tr>';
                ++count;
            }

            if (count == 1) { x += '<tr><td><div style=padding:6px>&nbsp;<i>' + "Sin Miembros" + '</i><div></div></div></td><td></td></tr>'; }

            x += '</tbody></table><br />';

            // Display all device groups for this user group
            count = 1;
            var deviceGroupCount = 0, newDeviceGroup = false;
            for (var i in meshes) { if (currentUserGroup._id.split('/')[1] != meshes[i]._id.split('/')[1]) continue; deviceGroupCount++; if ((currentUserGroup.links == null) || (currentUserGroup.links[i] == null)) { newDeviceGroup = true; } }
            if ((deviceGroupCount > 0) && (newDeviceGroup)) { x += '<button class="btn btn-primary btn-sm me-2 mb-1"onclick="return p20showAddMeshUserDialog(3)"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Grupo de Dispositivos" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Grupos de Dispositivos en Común" + '</th><th scope=col></th></tr>';
            if (currentUserGroup.links) {
                var omeshes = [];
                for (var i in currentUserGroup.links) { if (i.startsWith('mesh/')) { if (meshes[i] != null) { omeshes.push(meshes[i]); } } }
                omeshes = getOrderedList(omeshes, 'name');
                for (var i in omeshes) {
                    var cr = 0, mesh = omeshes[i], r = currentUserGroup.links[mesh._id].rights, trash = '', rights = makeDeviceGroupRightsString(r);
                    if ((userinfo.links) && (userinfo.links[mesh._id] != null) && (userinfo.links[mesh._id].rights != null)) { cr = userinfo.links[mesh._id].rights; }
                    var meshname = '<i>' + "Grupo de Dispositivos Desconocido" + '</i>';
                    if (mesh) { meshname = '<a href=# onclick=\'gotoMesh("' + mesh._id + '");haltEvent(event);\'>' + EscapeHtml(mesh.name) + '</a>'; } else { }
                    if ((cr & 2) != 0) {
                        trash = '<a href=# onclick=\'return p51removeMeshFromUserGroup(event,"' + encodeURIComponentEx(mesh._id) + '")\' title="' + "Remover los permisos del grupo de usuarios a este grupo de dispositivos" + '" style=cursor:pointer><i class="fa-solid fa-trash text-danger hoverButton"></i></a>';
                        rights = '<span style=cursor:pointer onclick=p20showAddMeshUserDialog(3,"' + encodeURIComponentEx(mesh._id) + '")>' + rights + ' <img class=hoverButton style=cursor:pointer src=images/link5.png></span>';
                    }
                    x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%><div title="' + "Grupo de Dispositivos" + '" class=m99></div><div>&nbsp;' + meshname + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
                }
            }
            if (count == 1) { x += '<tr><td><div style=padding:6px>&nbsp;<i>' + "No hay grupos de dispositivos en común" + '</i><div></div></div></td><td></td></tr>'; }
            x += '</tbody></table>';

            // Display all devices for this user group
            count = 1;
            x += '<br />';
            if (currentUserGroup._id.split('/')[1] == userinfo._id.split('/')[1]) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick="return p20showAddMeshUserDialog(7)"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Dispositivo" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Dispositivos en Común" + '</th><th scope=col></th></tr>';
            if (currentUserGroup.links) {
                var onodes = [];
                for (var i in currentUserGroup.links) { if (i.startsWith('node/')) { var node = getNodeFromId(i); if (node != null) { onodes.push(node); } } }
                onodes = getOrderedList(onodes, 'name');
                for (var i in onodes) {
                    var node = onodes[i], r = currentUserGroup.links[node._id].rights, trash = '', rights = makeUserDeviceRightsString(r), cr = GetNodeRights(node);
                    var nodename = '<i>' + "Dispositivo Desconocido" + '</i>';
                    if (node) { nodename = '<a href=# onclick=\'gotoDevice("' + node._id + '");haltEvent(event);\'>' + EscapeHtml(node.name) + '</a>'; } else { }
                    if ((cr & 2) != 0) {
                        trash = '<a href=# onclick=\'return p51removeDeviceFromUserGroup(event,"' + encodeURIComponentEx(node._id) + '")\' title="' + "Remover los permisos de grupo de usuarios para este dispositivo" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                        rights = '<span style=cursor:pointer onclick=p20showAddMeshUserDialog(7,"' + encodeURIComponentEx(node._id) + '")>' + rights + ' <img class=hoverButton style=cursor:pointer src=images/link5.png></span>';
                    }
                    x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%><div title="' + "Grupo de Dispositivos" + '" class=m99></div><div>&nbsp;' + nodename + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
                }
            }
            if (count == 1) { x += '<tr><td><div style=padding:6px>&nbsp;<i>' + "Sin dispositivos en común" + '</i><div></div></div></td><td></td></tr>'; }
            x += '</tbody></table>';

            if (((currentUserGroup.membershipType == null) || (usercount == 0)) && ((userinfo.siteadmin & 256) != 0)) {
                x += '<div style=font-size:small;text-align:right><span><a href=# onclick=p51showDeleteUserGroupDialog() style=cursor:pointer>' + "Eliminar Grupo de Usuarios" + '</a></span></div>';
            }

            QH('p51group2', x);
            go(51);

            // Change the URL
            var urlviewmode = '';
            if (((features & 0x10000000) == 0) && (xxcurrentView >= 51) && (xxcurrentView <= 59) && (currentUserGroup != null)) {
                urlviewmode = '?viewmode=' + xxcurrentView + '&gotougrp=' + ((serverinfo.crossDomain) ? currentUserGroup._id : currentUserGroup._id.split('/')[2]);
                for (var i in urlargs) { urlviewmode += ('&' + i + '=' + urlargs[i]); }
                try { window.history.replaceState({}, document.title, window.location.pathname + urlviewmode); } catch (ex) { }
            }
        }

        function p51edituserGroupFeatures() {
            if (xxdialogMode) return;
            var flags = (currentUserGroup.flags) ? currentUserGroup.flags : 0, x = ''; // Flags: 2 = Session Recording
            if (serverinfo.userGroupsSessionRecording == 1) {
                x += '<div><label><input type=checkbox class="form-check-input me-2" id=d51flag1 ' + ((flags & 2) ? 'checked' : '') + '>' + "Grabar sesiones" + '</label><br></div>';
            }
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Editar características del grupo de usuarios", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p51edituserGroupFeaturesEx);
        }

        // Send to the server the new user's real name
        function p51edituserGroupFeaturesEx() {
            // Setup user flags
            var flags = 0; // Flags: 2 = Session Recording
            if ((serverinfo.userGroupsSessionRecording == 1) && Q('d51flag1').checked) { flags += 2; }
            meshserver.send({ action: 'editusergroup', ugrpid: currentUserGroup._id, flags: flags });
        }

        function p51removeDeviceFromUserGroup(e, nodeid) {
            if (xxdialogMode) return;
            var node = getNodeFromId(decodeURIComponent(nodeid));
            if (node == null) return;
            setModalContent('xxAddAgent', "Remover Permisos del Dispositivo", format("¿Confirmar la eliminación de los permisos de acceso para el dispositivo \"{0}\"?", EscapeHtml(node.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p51removeDeviceFromUserGroupEx(3, node._id));
        }

        function p51removeDeviceFromUserGroupEx(b, nodeid) {
            meshserver.send({ action: 'adddeviceuser', nodeid: nodeid, userids: [currentUserGroup._id], rights: 0, remove: true });
        }

        function p51removeMeshFromUserGroup(e, meshid) {
            if (xxdialogMode) return;
            var mesh = meshes[decodeURIComponent(meshid)];
            if (mesh == null) return;
            setModalContent('xxAddAgent', "Remover Permisos de Grupo de Dispositivos", format("¿Confirmar la eliminación de los permisos de acceso para el grupo de dispositivos \"{0}\"?", EscapeHtml(mesh.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => { meshserver.send({ action: 'removemeshuser', meshid: decodeURIComponent(meshid), userid: currentUserGroup._id }); });;
        }
        function p51editgroup(focus, nameReadOnly) {
            if (xxdialogMode) return;
            var x = addHtmlFormFloating("Nombre", '<input id=dp51name class="form-control" maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) ' + (nameReadOnly ? 'readonly disabled' : '') + '/>');
            x += addHtmlFormFloating("Descripción", '<textarea id=dp51desc class="form-control" maxlength=1024 style=width:100%;resize:none></textarea>');

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Editar Grupo de Usuarios", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p51editgroupEx);

            Q('dp51name').value = currentUserGroup.name;
            if (currentUserGroup.desc) Q('dp51desc').value = currentUserGroup.desc;
            p51editgroupValidate();
            if (focus == 2) { Q('dp51desc').focus(); } else { Q('dp51name').focus(); }
        }

        function p51editgroupEx() {
            meshserver.send({ action: 'editusergroup', ugrpid: currentUserGroup._id, name: Q('dp51name').value, desc: Q('dp51desc').value });
        }

        function p51editgroupValidate(e) {
            QE('idx_dlgOkButton', Q('dp51name').value.length > 0); if (e && e.key == 'Enter') { Q('dp51desc').focus(); }
        }

        function p51showDeleteUserGroupDialog() {
            if (xxdialogMode) return false;
            var x = format("¿Eliminar Grupo de Usuarios {0}?", EscapeHtml(currentUserGroup.name)) + '<br /><br />';
            x += '<label><input id=p51check type=checkbox class="form-check-input me-2" onchange=p51validateDeleteGroupDialog() />' + "Confirmar" + '</label>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Eliminar Grupo de Usuarios", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p51showDeleteUserGroupDialogEx(xxdialogButtons, xxdialogTag);
            });

            p51validateDeleteGroupDialog();
            return false;
        }

        function p51validateDeleteGroupDialog() {
            QE('idx_dlgOkButton', Q('p51check').checked);
        }

        function p51showDeleteUserGroupDialogEx(buttons, tag) {
            meshserver.send({ action: 'deleteusergroup', ugrpid: currentUserGroup._id });
        }

        function p51deleteUser(e, id) {
            haltEvent(e);
            p51viewuserEx(2, decodeURIComponent(id));
            return false;
        }

        function p51viewuserEx(button, userid) {
            if (button != 2) return;
            var uname = userid.split('/')[2];
            if (users && users[userid]) { uname = users[userid].name; }
            if (userinfo._id == userid) { uname = userinfo.name; }
            setModalContent('xxAddAgent', "Remover Membresía de Usuario", format("¿Confirmar la eliminación del usuario \"{0}\"?", EscapeHtml(decodeURIComponent(uname))));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => { meshserver.send({ action: 'removeuserfromusergroup', ugrpid: currentUserGroup._id, userid: userid }); });
        }

        function p51showAddUserDialog() {
            if (xxdialogMode) return false;
            var x = "Permitir a los usuarios administrar este grupo de dispositivos y los dispositivos en este grupo.";
            if (features & 0x00080000) { x += " Los usuarios deben iniciar sesión en este servidor una vez antes de poder ser añadidos a un grupo de dispositivos." }
            x += '<br /><br /><div style=\'position:relative\'>';
            x += addHtmlFormFloating("Identificadores de Usuario", '<input id=dp51username maxlength=32 class="form-control" onchange=p51validateAddUserDialog() onkeyup=p51validateAddUserDialog() placeholder="user1, user2, user3" />');
            x += '<div id=dp51usersuggest class=suggestionBox style=\'top:30px;left:130px;display:none\'></div>';
            x += '</div><br>';

            setModalContent('xxAddAgent', "Agregar Usuarios al Grupo de Usuarios", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p51showAddUserDialogEx(3, xxdialogTag)
            });

            Q('dp51username').focus();
            p51validateAddUserDialog();
            return false;
        }

        function p51setname(name) {
            name = decodeURIComponent(name);
            var xusers = Q('dp51username').value.split(',');
            for (var i in xusers) { xusers[i] = xusers[i].trim(); }
            xusers[xusers.length - 1] = name;
            Q('dp51username').value = xusers.join(', ');
            p51validateAddUserDialog();
            return false;
        }

        function p51validateAddUserDialog() {
            var ok = true;
            if (Q('dp51username')) {
                var xusers = Q('dp51username').value.split(',');
                for (var i in xusers) {
                    var xuser = xusers[i] = xusers[i].trim();
                    if (xuser.length == 0) { ok = false; } else if (xuser.indexOf('"') >= 0) { ok = false; }
                }

                // Fill the suggestion box
                var showsuggestbox = false, exactMatch = false;
                if (users != null) {
                    var lastuser = xusers[xusers.length - 1].trim(), lastuserl = lastuser.toLowerCase(), matchingUsers = [];
                    if (lastuser.length > 0) {
                        for (var i in users) {
                            var userSplit = users[i]._id.split('/');
                            if ((currentUserGroup.domain == userSplit[1]) && (userSplit[2] === lastuserl)) { exactMatch = true; break; }
                            if ((users[i].name.toLowerCase().indexOf(lastuserl) >= 0) && (currentUserGroup.domain == userSplit[1])) { matchingUsers.push([users[i]._id, users[i].name]); if (matchingUsers.length >= 8) break; }
                        }
                        if ((exactMatch == false) && (matchingUsers.length > 0)) {
                            var x = '';
                            for (var i in matchingUsers) {
                                var sid = matchingUsers[i][0], sname = matchingUsers[i][1];
                                if (sid.split('/')[2] == sname.toLowerCase()) {
                                    x += '<div class=suggestionBoxItem onclick=\'p51setname("' + encodeURIComponentEx(sid.split('/')[2]) + '")\'>' + EscapeHtml(sname) + '</div>';
                                } else {
                                    x += '<div class=suggestionBoxItem onclick=\'p51setname("' + encodeURIComponentEx(sid.split('/')[2]) + '")\'><div>' + EscapeHtml(sname) + '</div><div class=suggestionBoxSubItem>' + EscapeHtml(sid.split('/')[2]) + '</div></div>';
                                }
                            }
                            QH('dp51usersuggest', x);
                            showsuggestbox = true;
                        }
                    }
                }
                QV('dp51usersuggest', showsuggestbox);
            }

            QE('idx_dlgOkButton', ok);
        }

        function p51showAddUserDialogEx(b, t) {
            if (t == null) {
                var users = Q('dp51username').value.split(','), users2 = [];
                for (var i in users) { users2.push(users[i].trim()); }
                meshserver.send({ action: 'addusertousergroup', ugrpid: currentUserGroup._id, usernames: users2 });
            } else {
                meshserver.send({ action: 'addusertousergroup', ugrpid: currentUserGroup._id, usernames: [t.split('/')[2]] });
            }
        }



        //
        // MY USER GENERAL
        //

        var currentUser = null;
        function gotoUser(userid, force, event) {
            if (xxdialogMode && !force) return;
            if ((event != null) && (event.originalTarget != null) && (event.originalTarget.href != null)) return;
            var user = currentUser = users[decodeURIComponent(userid)];
            if (user == null) { go(4); return; }
            QH('p30userName', ' - ' + EscapeHtml(user.name));
            QH('p31userName', ' - ' + EscapeHtml(user.name));
            var self = (user._id == userinfo._id), activeSessions = 0;
            if (wssessions != null && wssessions[user._id]) { activeSessions = wssessions[user._id]; }

            // Update account picture
            if ((user.flags != null) && (user.flags & 1)) {
                QV('MainUserImage', false);
                QV('MainUserImageEx', true);
                if (user.accountImageRnd == null) { user.accountImageRnd = Math.floor(Math.random() * 9999999999); }
                Q('MainUserImageEx').src = 'userimage.ashx?id=' + (user._id.split('/')[2]) + '&rnd=' + user.accountImageRnd;
            } else {
                // Change user grayscale
                Q('MainUserImage').classList.remove('gray');
                if (activeSessions == 0) { Q('MainUserImage').classList.add('gray'); }
                QV('MainUserImage', true);
                QV('MainUserImageEx', false);
            }

            // Add user auth strategy
            var shortuserid = user._id.split('/')[2];
            if (shortuserid.startsWith('~twitter:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/twitter64.png'; }
            else if (shortuserid.startsWith('~google:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/google64.png'; }
            else if (shortuserid.startsWith('~github:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/github64.png'; }
            else if (shortuserid.startsWith('~azure:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/azure64.png'; }
            else if (shortuserid.startsWith('~oidc:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/oidc64.png'; }
            else if (shortuserid.startsWith('~jumpcloud:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/jumpcloud64.png'; }
            else if (shortuserid.startsWith('~intel:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/intel64.png'; }
            else if (shortuserid.startsWith('~:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/generic64.png'; }
            else { QV('p30userAuthServiceLogo', false); }

            // Server permissions
            var msg = [], premsg = '';
            if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) { premsg = '<img src="images/padlock12.png" height=12 width=8 title="' + "La cuenta esta bloqueada" + '" style="margin-top:2px" /> '; msg.push("Cuenta bloqueada"); }
            if ((user.siteadmin == null) || ((user.siteadmin & (0xFFFFFFFF - 1248)) == 0)) { msg.push("Sin permisos en el servidor"); } else if (user.siteadmin == 8) { msg.push("Acceso a archivos del servidor"); } else if (user.siteadmin == 0xFFFFFFFF) { msg.push("Administrador total"); } else { msg.push("Permisos parciales"); }
            if ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & (64 + 128 + 1024)) != 0)) { msg.push("Restricciones"); }

            // Show user attributes
            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Nombre", user.name); } // If title bar is hidden, display the user name here
            var email = user.email ? EscapeHtml(user.email) : '<i>' + "No establecido" + '</i>', everify = '';
            var realname = user.realname ? EscapeHtml(user.realname) : '<i>' + "No establecido" + '</i>';
            if (serverinfo.emailcheck) { everify = ((user.emailVerified == true) ? '<b style=color:green;cursor:pointer title="' + "El correo electrónico está verificado" + '">&#x2713</b> ' : '<b style=color:red;cursor:pointer title="' + "Correo electrónico no verificado" + '">&#x2717;</b> '); }

            if ((serverinfo.crossDomain) || (debugmode != 0)) {
                var d = user._id.split('/')[1];
                x += addDeviceAttribute("Dominio", ((d != '') ? EscapeHtml(d) : ('<i>' + "Por Defecto" + '</i>')));
                x += addDeviceAttribute("Identificador de Usuario", EscapeHtml(user._id));
            } else {
                if (user.name.toLowerCase() != user._id.split('/')[2]) { x += addDeviceAttribute("Identificador de Usuario", EscapeHtml(user._id.split('/')[2])); }
            }

            var emailLink = '';
            if (user.email) { emailLink = ' <a href="mailto:' + EscapeHtml(user.email) + '" \'><i class="fa-solid fa-arrow-up-right-from-square fa-xs"></i></a>'; }
            if (((user.siteadmin != 0xFFFFFFFF) || (userinfo.siteadmin == 0xFFFFFFFF))) { // If we are not site admin, we can't change a admin email or real name
                x += addDeviceAttribute("Correo electrónico", '<span role=button onclick=p30showUserEmailChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + everify + email + emailLink + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
                x += addDeviceAttribute("Nombre Real", '<span role=button onclick=p30showUserRealNameChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + realname + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            } else {
                x += addDeviceAttribute("Correo electrónico", everify + email + emailLink);
                x += addDeviceAttribute("Nombre Real", realname);
            }

            if ((features & 0x02000000) || (user.phone != null)) { // If SMS is enabled on the server or user has a phone number
                x += addDeviceAttribute("Número de Teléfono", '<span role=button onclick=p30editPhone()>' + (user.phone ? user.phone : ('<i>' + "Ninguno" + '</i>')) + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            }

            if ((features2 & 0x02000000) || (user.msghandle != null)) { // If user messaging is enabled on the server or user has a messaging handle
                x += addDeviceAttribute("Mensajería", '<span role=button onclick=p30editMessaging()><i class="fa-solid fa-comment fa-xs" title="' + "Mensajería habilitada" + '" style="margin-top:2px"></i> ' + (user.msghandle ? user.msghandle : ('<i>' + "Ninguno" + '</i>')) + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            }

            // Display features
            var userFeatures = [];
            if ((serverinfo.usersSessionRecording == 1) && (user.flags) && (user.flags & 2)) { userFeatures.push("Grabar Sesiones"); }
            if (user.removeRights) {
                if ((user.removeRights & 0x00000008) != 0) { userFeatures.push("No hay Control remoto"); } else {
                    if ((user.removeRights & 0x00010000) != 0) { userFeatures.push("Sin Escritorio"); }
                    else if ((user.removeRights & 0x00000100) != 0) { userFeatures.push("Vista de escritorio solamente"); }
                    if ((user.removeRights & 0x00000200) != 0) { userFeatures.push("Sin Terminal"); }
                    if ((user.removeRights & 0x00000400) != 0) { userFeatures.push("Sin Archivos"); }
                }
                if ((user.removeRights & 0x00000010) != 0) { userFeatures.push("Sin consola"); }
                if ((user.removeRights & 0x00008000) != 0) { userFeatures.push("No hay Desinstalar"); }
                if ((user.removeRights & 0x00020000) != 0) { userFeatures.push("No hay Comando remoto"); }
                if ((user.removeRights & 0x00000040) != 0) { userFeatures.push("No hay Despertar"); }
                if ((user.removeRights & 0x00040000) != 0) { userFeatures.push("No hay Reiniciar/Apagar"); }
            }
            userFeatures = userFeatures.join(', ');
            if (userFeatures == '') { userFeatures = '<i>' + "Ninguno" + '</i>'; }
            x += addDeviceAttribute("Caracteristicas", addLink(userFeatures, 'p20edituserfeatures()'));

            x += addDeviceAttribute("Permisos del Servidor", '<span role=button onclick=\'return showUserAdminDialog(event,"' + encodeURIComponentEx(user._id) + '")\'>' + premsg + msg.join(', ') + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            if (user.quota) x += addDeviceAttribute("Cuota del Servidor", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
            x += addDeviceAttribute("Creación", printDateTime(new Date(user.creation * 1000)));
            if (user.login) x += addDeviceAttribute("Último Ingreso", printDateTime(new Date(user.login * 1000)));
            if (user.passchange == -1) { x += addDeviceAttribute("Contraseña", "Se cambiará en el próximo inicio de sesión."); }
            else if (user.passchange) { x += addDeviceAttribute("Contraseña", format("Última modificación: {0}", printDateTime(new Date(user.passchange * 1000)))); }

            // Device Groups
            var linkCount = 0, linkCountStr = '<i>' + "Ninguno" + '<i>';
            if (user.links) {
                for (var i in user.links) { if (i.startsWith('mesh/')) { linkCount++; } }
                if (linkCount == 1) { linkCountStr = "1 grupo"; } else if (linkCount > 1) { linkCountStr = format("{0} grupos", linkCount); }
            }
            x += addDeviceAttribute("Grupos de Dispositivos", linkCountStr);

            // Administrative Realms
            if ((userinfo.siteadmin == 0xFFFFFFFF) || (userinfo.siteadmin & 2)) {
                var xuserGroups = '<i>' + "Ninguno" + '</i>';
                if (user.groups) { xuserGroups = ''; for (var i in user.groups) { xuserGroups += '<span class="tagSpan">' + EscapeHtml(user.groups[i]) + '</span>'; } }
                x += addDeviceAttribute("Reinos del Administrador", addLinkConditional(xuserGroups, 'showUserGroupDialog(event,"' + encodeURIComponentEx(user._id) + '")', (userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.groups == null) && (userinfo._id != user._id) && (user.siteadmin != 0xFFFFFFFF))));
            }

            // Display device user consent
            {
                var meshFeatures = [], consent = 0;
                if (user.consent) { consent = user.consent; }
                if (serverinfo.consent) { consent |= serverinfo.consent; }
                if ((consent & 0x0040) && (consent & 0x0008)) { meshFeatures.push("Mensaje de Escritorio + Barra de Herramientas"); } else if (consent & 0x0040) { meshFeatures.push("Barra de Herramientas de Escritorio"); } else if (consent & 0x0008) { meshFeatures.push("Mensaje de Escritorio"); } else { if (consent & 0x0001) { meshFeatures.push("Notificación de Escritorio"); } }
                if (consent & 0x0010) { meshFeatures.push("Mensaje en Terminal"); } else { if (consent & 0x0002) { meshFeatures.push("Notificación de Terminal"); } }
                if (consent & 0x0020) { meshFeatures.push("Solicitud de Archivos"); } else { if (consent & 0x0004) { meshFeatures.push("Notificación de Archivos"); } }
                if (consent == 7) { meshFeatures = ["Siempre Notificar"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Siempre Preguntar"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Ninguno" + '</i>'; }
                x += addDeviceAttribute("Consentimiento del Usuario", addLinkConditional(meshFeatures, 'p20editmeshconsent(2)', true));
            }

            var multiFactor = 0;
            if ((user.otpsecret > 0) || (user.otphkeys > 0) || (user.otpekey > 0) || (user.otpduo > 0)) {
                multiFactor = 1;
                var factors = [];
                if (user.otpsecret > 0) { factors.push("Aplicación de Autenticación"); }
                if (user.otphkeys > 0) { factors.push("Llave de Seguridad"); }
                if (user.otpekey > 0) { factors.push("Correo electrónico"); }
                if (user.otpduo > 0) { factors.push("Duo"); }
                if (user.otpkeys > 0) { factors.push("Códigos de Respaldo"); }
                if (user.otpdev > 0) { factors.push("Dispositivo Push"); }
                if ((user.phone != null) && (features & 0x04000000)) { factors.push("SMS"); }
                if ((user.msghandle != null) && (features2 & 0x04000000)) { factors.push("Mensajería"); }
                x += addDeviceAttribute("Seguridad", '<i class="fa-solid fa-key" title="' + "Segundo factor de autenticación habilitado" + '" style="margin-top:2px"></i> ' + factors.join(', '));
            }

            x += '</table></div><br />';

            // Add action buttons
            x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Notas" + '" title="' + "Ver notas sobre este usuario" + '" onclick=showNotes(false,"' + encodeURIComponentEx(user._id) + '") />';
            if (user.phone && (features & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "SMS" + '" title="' + "Enviar un mensaje SMS a este usuario" + '" onclick=showSendSMS("' + encodeURIComponentEx(user._id) + '") />'; }
            if (user.msghandle && (features2 & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Mensaje" + '" title="' + "Enviar mensaje a este usuario" + '" onclick=showSendMessage("' + encodeURIComponentEx(user._id) + '") />'; }
            if ((typeof user.email == 'string') && (user.emailVerified === true) && (features & 0x00000040)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Correo electrónico" + '" title="' + "Enviar un mensaje de correo electrónico a este usuario" + '" onclick=showSendEmail("' + encodeURIComponentEx(user._id) + '") />'; }
            if (!self && ((activeSessions > 0) || ((features2 & 8) && (user.webpush)))) {
                x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Notificar" + '" title="' + "Enviar notificación de usuario" + '" onclick=showUserAlertDialog(event,"' + encodeURIComponentEx(user._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Chat" + '" title="' + "Chat" + '" onclick=userChat(event,"' + encodeURIComponentEx(user._id) + '","' + encodeURIComponentEx(user.name) + '") />';
                if ((activeSessions > 0) && (serverinfo != null) && (serverinfo.altmessenging != null)) {
                    for (var i in serverinfo.altmessenging) {
                        var am = serverinfo.altmessenging[i];
                        if ((am.type == null) || (am.type == 'user')) {
                            x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + EscapeHtml(am.name) + '" onclick=altUserChat(event,"' + encodeURIComponentEx(user._id) + '","' + encodeURIComponentEx(user.name) + '",' + i + ') />';
                        }
                    }
                }
            }

            // Setup the panel
            QH('p30html', x);

            // Draw the user timeline
            drawUserPermissions();

            // Check if we can change password / delete this user
            var userAdminRights = (((userinfo.siteadmin != null) && (userinfo.siteadmin & 2) && (user.siteadmin != 0xFFFFFFFF)) || (userinfo.siteadmin == 0xFFFFFFFF));

            // Show bottom buttons
            x = '<div style=float:right;font-size:small>';
            if (userAdminRights && (userinfo._id != user._id)) { x += '<a href=# style=cursor:pointer onclick=\'return p30showDeleteUserDialog()\' title="' + "Remover este usuario" + '">' + "Eliminar Usuario" + '</a>'; }
            x += '</div><div style=font-size:small>';
            // If user admin rights and not SSPI/LDAP and UserID does not start with ~, show change password
            if (userAdminRights && ((features & 0x00080000) == 0) && (user._id.split('/')[2][0] != '~')) {
                x += '<a href=# style=cursor:pointer onclick=\'return p30showUserChangePassDialog(' + multiFactor + ')\' title="' + "Cambiar la contraseña para este usuario" + '">' + "Cambiar Contraseña" + '</a>';
                x += ' <a href=# style=cursor:pointer onclick=\'return p30viewPreviousLogins()\' title="' + "Ver inicios de sesión anteriores de éste usuario" + '">' + "Inicios de Sesión Anteriores" + '</a>';
            }
            x += '</div><br>'
            QH('p30html3', x);

            // Update user's connection state
            x = '';
            if (activeSessions == 1) { x = "1 sesión activa"; } else if (activeSessions > 1) { x = format("{0} sesiones activas", activeSessions); }
            QH('MainUserState', x);

            go(30);

            // Update user events (TODO: do this only if we change users)
            QH('p31events', '');
            refreshUsersEvents();

            // Change the URL
            var urlviewmode = '';
            if (((features & 0x10000000) == 0) && (xxcurrentView >= 30) && (xxcurrentView <= 39) && (currentUser != null)) {
                urlviewmode = '?viewmode=' + xxcurrentView + '&gotouser=' + ((serverinfo.crossDomain) ? currentUser._id : currentUser._id.split('/')[2]);
                for (var i in urlargs) { urlviewmode += ('&' + i + '=' + urlargs[i]); }
                try { window.history.replaceState({}, document.title, window.location.pathname + urlviewmode); } catch (ex) { }
            }
        }

        function p30editPhone() {
            if (xxdialogMode) return;
            var x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
            x += '<td style=width:100%;text-align:center>' + "Número de teléfono con capacidad de SMS para este usuario." + '<br />' + "Deja en blanco para ninguno.";
            x += '<br /><br /><div style=width:100%;text-align:center>' + "Número de teléfono:" + ' <input type=tel pattern="[0-9]" autocomplete="tel" value="' + (currentUser.phone ? currentUser.phone : '') + '" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=p30editPhoneValidate() onkeypress="if (event.key==\'Enter\') p30editPhoneValidate(1)"></div></table>';
            xxdialogButtons = 3; xxdialogTag = 'verifyPhone';
            setModalContent('xxAddAgent', "Notificaciones Telefónicas", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p30editPhoneEx);
            Q('d2phoneinput').focus();
            p30editPhoneValidate();
        }

        function p30editMessaging() {
            if (xxdialogMode) return;
            var x = '<table style=width:100%><tr><td style=width:56px;vertical-align:top><img src="images/messaging40.png" style=padding:8px>';
            x += '<td style=width:100%>' + "Cuenta de Mensajería para este usuario.";
            var y = '<select id=d2serviceselect style=width:160px;margin-left:8px onchange=p30editMessagingValidate()><option value=0>' + "Ninguno" + '</option>';
            if ((serverinfo.userMsgProviders & 1) != 0) { y += '<option value=1>' + "Telegram" + '</option>'; }
            if ((serverinfo.userMsgProviders & 4) != 0) { y += '<option value=4>' + "Discord" + '</option>'; }
            if ((serverinfo.userMsgProviders & 8) != 0) { y += '<option value=8>' + "XMPP" + '</option>'; }
            if ((serverinfo.userMsgProviders & 16) != 0) { y += '<option value=16>' + "CallMeBot" + '</option>'; }
            if ((serverinfo.userMsgProviders & 32) != 0) { y += '<option value=32>' + "Pushover" + '</option>'; }
            if ((serverinfo.userMsgProviders & 64) != 0) { y += '<option value=64>' + "ntfy" + '</option>'; }
            if ((serverinfo.userMsgProviders & 128) != 0) { y += '<option value=128>' + "Zulip" + '</option>'; }
            if ((serverinfo.userMsgProviders & 256) != 0) { y += '<option value=256>' + "Slack" + '</option>'; }
            y += '</select>';
            x += '<table style=margin-top:12px><tr><td>' + "Servicio" + '<td>' + y;
            x += '<tr><td>' + "Usuario" + '<td><input maxlength=1024 style=width:160px;margin-left:8px id=d2handleinput onKeyUp=p30editMessagingValidate() onkeypress="if (event.key==\'Enter\') p30editMessagingValidate(1)">';
            x += '</table>';
            if (serverinfo.discordUrl) { x += '<div id=d2discordurl style=display:none><br /><a href=' + serverinfo.discordUrl + ' target="_discord">' + "Unete a este canal de Discord para recibir notificaciones." + '</a></div>' }
            x += '<div id=d2callmebotinfo style=display:none><br /><a href=https://www.callmebot.com/blog/free-api-signal-send-messages/ target="_callmebot">' + "Signal" + '</a>, <a href=https://www.callmebot.com/blog/free-api-whatsapp-messages/ target="_callmebot">' + "Whatsapp" + '</a>, <a href=https://www.callmebot.com/blog/free-api-facebook-messenger/ target="_callmebot">' + "Facebook" + '</a>, <a href=https://www.callmebot.com/blog/telegram-text-messages/ target="_callmebot">' + "Telegram" + '</a></div>';
            x += '<div id=d2pushoverinfo style=display:none><br /><a href=https://pushover.net/ target="_pushover">' + "Informacion en Pushover.net" + '</a></div>';
            x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Servicio gratuito en ntfy.sh" + '</a></div>';
            x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Slack Webhook Setup" + '</a></div>';
            xxdialogButtons = 3; xxdialogTag = 'verifyMessaging';
            setModalContent('xxAddAgent', "Notificaciones de Mensajería", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p30editMessagingEx);
            Q('d2handleinput').focus();
            if (currentUser.msghandle) {
                if (currentUser.msghandle.startsWith('telegram:') && ((serverinfo.userMsgProviders & 1) != 0)) { Q('d2serviceselect').value = 1; Q('d2handleinput').value = currentUser.msghandle.substring(10); }
                if (currentUser.msghandle.startsWith('discord:') && ((serverinfo.userMsgProviders & 4) != 0)) { Q('d2serviceselect').value = 4; Q('d2handleinput').value = currentUser.msghandle.substring(8); }
                if (currentUser.msghandle.startsWith('xmpp:') && ((serverinfo.userMsgProviders & 8) != 0)) { Q('d2serviceselect').value = 8; Q('d2handleinput').value = currentUser.msghandle.substring(5); }
                if (currentUser.msghandle.startsWith('callmebot:') && ((serverinfo.userMsgProviders & 16) != 0)) {
                    Q('d2serviceselect').value = 16;
                    var toData = currentUser.msghandle.substring(10).split('|');
                    if ((toData[0] == 'signal') && (toData.length == 3)) { Q('d2handleinput').value = 'https://signal.callmebot.com/signal/send.php?phone=' + decodeURIComponent(toData[1]) + '&apikey=' + decodeURIComponent(toData[2]); }
                    if ((toData[0] == 'whatsapp') && (toData.length == 3)) { Q('d2handleinput').value = 'https://api.callmebot.com/whatsapp.php?phone=' + decodeURIComponent(toData[1]) + '&apikey=' + decodeURIComponent(toData[2]); }
                    if ((toData[0] == 'facebook') && (toData.length == 2)) { Q('d2handleinput').value = 'https://api.callmebot.com/facebook/send.php?apikey=' + decodeURIComponent(toData[1]); }
                }
                if (currentUser.msghandle.startsWith('pushover:') && ((serverinfo.userMsgProviders & 32) != 0)) { Q('d2serviceselect').value = 32; Q('d2handleinput').value = currentUser.msghandle.substring(9); }
                if (currentUser.msghandle.startsWith('ntfy:') && ((serverinfo.userMsgProviders & 64) != 0)) { Q('d2serviceselect').value = 64; Q('d2handleinput').value = currentUser.msghandle.substring(5); }
                if (currentUser.msghandle.startsWith('zulip:') && ((serverinfo.userMsgProviders & 128) != 0)) { Q('d2serviceselect').value = 128; Q('d2handleinput').value = currentUser.msghandle.substring(6); }
                if (currentUser.msghandle.startsWith('slack:') && ((serverinfo.userMsgProviders & 256) != 0)) { Q('d2serviceselect').value = 256; Q('d2handleinput').value = currentUser.msghandle.substring(6); }
            }
            p30editMessagingValidate();
        }

        function p30editMessagingValidate(x) {
            QE('d2handleinput', Q('d2serviceselect').value != 0);
            if (serverinfo.discordUrl) { QV('d2discordurl', Q('d2serviceselect').value == 4); }
            QV('d2callmebotinfo', Q('d2serviceselect').value == 16);
            QV('d2pushoverinfo', Q('d2serviceselect').value == 32);
            QV('d2ntfyinfo', Q('d2serviceselect').value == 64);
            QV('d2slackinfo', Q('d2serviceselect').value == 256);
            if (Q('d2serviceselect').value == 0) { Q('d2handleinput')['placeholder'] = ''; }
            else if (Q('d2serviceselect').value == 4) { Q('d2handleinput')['placeholder'] = "Usuario:0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "nombredeusuario@servidor.com"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Clave de usuario"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Tema"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "nombredeusuario@ejemplo.com"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Nombre de Usuario"; }
            if (x == 1) { dialogclose(1); }
        }

        // Send to the server the user's messaging account
        function p30editMessagingEx() {
            var handle = null;
            if ((Q('d2handleinput').value == '') || (Q('d2serviceselect').value == 0)) { handle = ''; }
            else if (Q('d2serviceselect').value == 1) { handle = 'telegram:@' + Q('d2handleinput').value; }
            else if (Q('d2serviceselect').value == 4) { handle = 'discord:' + Q('d2handleinput').value; }
            else if (Q('d2serviceselect').value == 8) { handle = 'xmpp:' + Q('d2handleinput').value; }
            else if (Q('d2serviceselect').value == 16) { handle = 'callmebot:' + Q('d2handleinput').value; }
            else if (Q('d2serviceselect').value == 32) { handle = 'pushover:' + Q('d2handleinput').value; }
            else if (Q('d2serviceselect').value == 64) { handle = 'ntfy:' + Q('d2handleinput').value; }
            else if (Q('d2serviceselect').value == 128) { handle = 'zulip:' + Q('d2handleinput').value; }
            else if (Q('d2serviceselect').value == 256) { handle = 'slack:' + Q('d2handleinput').value; }
            if (handle != null) { meshserver.send({ action: 'edituser', id: currentUser._id, msghandle: handle }); }
        }

        function p20edituserfeatures() {
            if (xxdialogMode) return;
            var flags = (currentUser.flags) ? currentUser.flags : 0, x = ''; // Flags: 1 = Account Image, 2 = Session Recording
            var removeRights = (currentUser.removeRights) ? currentUser.removeRights : 0, x = ''; // Remove Device Group Rights
            if (serverinfo.usersSessionRecording == 1) {
                x += '<div><label><input type=checkbox id=d20flag1 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((flags & 2) ? 'checked' : '') + '>' + "Grabar sesiones" + '</label><br></div>';
            }
            x += '<div><label><input type=checkbox id=d20flag7 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000008) ? 'checked' : '') + '>' + "No hay Control remoto" + '</label><br></div>';
            x += '<div style=margin-left:8px><label><input type=checkbox id=d20flag2 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00010000) ? 'checked' : '') + '>' + "Sin Acceso al Escritorio" + '</label><br></div>';
            x += '<div style=margin-left:16px><label><input type=checkbox id=d20flag3 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000100) ? 'checked' : '') + '>' + "Sólo Vista Remota" + '</label><br></div>';
            x += '<div style=margin-left:8px><label><input type=checkbox id=d20flag4 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000200) ? 'checked' : '') + '>' + "Sin Acceso a la Terminal" + '</label><br></div>';
            x += '<div style=margin-left:8px><label><input type=checkbox id=d20flag5 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000400) ? 'checked' : '') + '>' + "Sin Acceso a Archivos" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag6 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000010) ? 'checked' : '') + '>' + "Sin consola de agente" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag8 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00008000) ? 'checked' : '') + '>' + "No hay Desinstalar" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag9 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00020000) ? 'checked' : '') + '>' + "No hay Comando remoto" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag10 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000040) ? 'checked' : '') + '>' + "No hay Despertar" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag11 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00040000) ? 'checked' : '') + '>' + "No hay Reiniciar/Apagar" + '</label><br></div>';

            setModalContent('xxAddAgent', "Editar funciones de usuario", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p20edituserfeaturesEx);

            p20edituserfeaturesValidate();
        }

        function p20edituserfeaturesValidate() {
            QE('d20flag2', !Q('d20flag7').checked);
            QE('d20flag3', !Q('d20flag7').checked && !Q('d20flag2').checked);
            QE('d20flag4', !Q('d20flag7').checked);
            QE('d20flag5', !Q('d20flag7').checked);
        }

        // Send to the server the new user's real name
        function p20edituserfeaturesEx() {
            // Setup user flags
            var flags = (currentUser.flags) ? currentUser.flags : 0; // Flags: 1 = Account Image, 2 = Session Recording
            var f = flags & 1;
            if ((serverinfo.usersSessionRecording == 1) && Q('d20flag1').checked) { f += 2; }

            // Setup user permission removal
            var r = 0;
            if (Q('d20flag7').checked) { r += 0x00000008; } else {
                if (Q('d20flag2').checked) { r += 0x00010000; }
                else if (Q('d20flag3').checked) { r += 0x00000100; }
                if (Q('d20flag4').checked) { r += 0x00000200; }
                if (Q('d20flag5').checked) { r += 0x00000400; }
            }
            if (Q('d20flag6').checked) { r += 0x00000010; }
            if (Q('d20flag8').checked) { r += 0x00008000; }
            if (Q('d20flag9').checked) { r += 0x00020000; }
            if (Q('d20flag10').checked) { r += 0x00000040; }
            if (Q('d20flag11').checked) { r += 0x00040000; }
            meshserver.send({ action: 'edituser', id: currentUser._id, flags: f, removeRights: r });
        }

        function p30editPhoneValidate(x) {
            var ok = (Q('d2phoneinput').value == '') || (isPhoneNumber(Q('d2phoneinput').value));
            QE('idx_dlgOkButton', ok);
            if ((x == 1) && ok) { dialogclose(1); }
        }

        // Send to the server the user's new phone number
        function p30editPhoneEx() { meshserver.send({ action: 'edituser', id: currentUser._id, phone: Q('d2phoneinput').value }); }

        // Display the user's real name change dialog box
        function p30showUserRealNameChangeDialog(event) {
            if (xxdialogMode) return false;
            var x = '';
            x += '<div class="form-floating mb-2"><input id=dp30realname class="form-control" placeholder="Real Name" maxlength=256 /><label for=dp30realname>Real Name</label></div>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', format("Cambiar Nombre Real de {0}", EscapeHtml(currentUser.name)), x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p30showUserRealNameChangeDialogEx);
            Q('dp30realname').focus();
            Q('dp30realname').value = (currentUser.realname ? currentUser.realname : '');
            return false;
        }

        // Send to the server the new user's real name
        function p30showUserRealNameChangeDialogEx() {
            meshserver.send({ action: 'edituser', id: currentUser._id, realname: Q('dp30realname').value });
        }

        // Display the user's email change dialog box
        function p30showUserEmailChangeDialog(event) {
            if (xxdialogMode) return false;
            var x = '';
            x += '<div class="form-floating mb-2"><input id=dp30email class="form-control" maxlength=256 onchange=p30validateEmail() onkeyup=p30validateEmail() placeholder=Email /><label for=dp30email>Email</label></div>';
            if (serverinfo.emailcheck) { x += addHtmlFormFloating("Estado", '<select id=dp30verified class="form-control" onchange=p30validateEmail()><option value=0>' + "No verificado" + '</option><option value=1>' + "Verficado" + '</option></select>'); }

            setModalContent('xxAddAgent', format("Cambiar Correo Electrónico para {0}", EscapeHtml(currentUser.name)), x);
            xxdialogButtons = 3;
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p30showUserEmailChangeDialogEx);

            Q('dp30email').focus();
            Q('dp30email').value = (currentUser.email ? currentUser.email : '');
            if (serverinfo.emailcheck) { Q('dp30verified').value = currentUser.emailVerified ? 1 : 0; }
            p30validateEmail();
            return false;
        }

        // Perform validation on the user's email change dialog box
        function p30validateEmail() {
            var v = Q('dp30email').value, x = v.split('@');
            x = (v.length == 0) || ((x.length == 2) && (x[0].length > 0) && (x[1].split('.').length > 1) && (x[1].length > 2) && (v.length < 1024) && ((v != userinfo.email) || ((serverinfo.emailcheck == true) && (Q('dp30verified').value != (userinfo.emailVerified ? 1 : 0)))));
            QE('idx_dlgOkButton', x);
        }

        // Send to the server the new user's email address and validation status
        function p30showUserEmailChangeDialogEx() {
            var x = { action: 'edituser', id: currentUser._id, email: Q('dp30email').value };
            if (serverinfo.emailcheck) { x.emailVerified = (Q('dp30verified').value == 1); }
            meshserver.send(x);
        }

        // Display the previous logins of this user
        function p30viewPreviousLogins() {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', "Inicios de Sesión Anteriores", 'Loading...');
            meshserver.send({ action: 'previousLogins', userid: currentUser._id });
        }

        // Display the user's password change dialog box
        function p30showUserChangePassDialog(multiFactor) {
            if (xxdialogMode) return;
            var x = '';
            x += '<div class="form-floating mb-2"><input id=p4pass1 type=password class="form-control" maxlength=256 onchange=p30showUserChangePassDialogValidate(1) onkeyup=p30showUserChangePassDialogValidate(1) placeholder=Password></input><label for=p4pass1>Password</label></div>';
            x += '<div class="form-floating mb-2"><input id=p4pass2 type=password class="form-control" maxlength=256 onchange=p30showUserChangePassDialogValidate(1) onkeyup=p30showUserChangePassDialogValidate(1) placeholder=Password></input><label for=p4pass2>Confirm Password</label></div>';

            if (features & 0x00010000) { x += '<div class="form-floating mb-2"><input id=p4hint type=text class="form-control" maxlength=256 placeholder="Password hint"></input><label for=p4hint>Password hint</label></div>'; }

            if (passRequirements) {
                var r = [], rc = 0;
                for (var i in passRequirements) { if ((i != 'reset') && (i != 'hint')) { r.push(i + ':' + passRequirements[i]); rc++; } }
                if (rc > 0) { x += '<div style=font-size:x-small;padding:6px>' + format("Requerimientos: {0}.", r.join(', ')) + '</div>'; }
            }

            x += '<div><label><input id=p4resetNextLogin class="form-check-input me-2" type=checkbox />' + "Forzar restablecimiento de contraseña en el próximo inicio de sesión." + '</label></div>';
            if (multiFactor == 1) { x += '<div><label><input id=p4twoFactorRemove class="form-check-input me-2" type=checkbox />' + "Remover toda la autenticación de segundo factor." + '</label></div>'; }
            setModalContent('xxAddAgent', format("Cambiar Contraseña de {0}", EscapeHtml(currentUser.name)), x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p30showUserChangePassDialogEx(3, multiFactor);
            });
            p30showUserChangePassDialogValidate();
            Q('p4pass1').focus();
            if (currentUser.passchange == -1) { Q('p4resetNextLogin').checked = true; }
        }

        function p30showUserChangePassDialogValidate() {
            var ok = true;
            if ((Q('p4pass1').value != '') || (Q('p4pass2').value != '')) {
                if (Q('p4pass1').value != Q('p4pass2').value) { ok = false; } else {
                    if (passRequirements) { if (checkPasswordRequirements(Q('p4pass1').value, passRequirements) == false) { ok = false; } }
                }
            }
            QE('idx_dlgOkButton', ok);
        }

        function p30showUserChangePassDialogEx(b, tag) {
            var removeMultiFactor = false;
            if ((tag == 1) && (Q('p4twoFactorRemove').checked == true)) { removeMultiFactor = true; }
            if (Q('p4pass1').value == Q('p4pass2').value) {
                var r = { action: 'changeuserpass', userid: currentUser._id, pass: Q('p4pass1').value, removeMultiFactor: removeMultiFactor, resetNextLogin: Q('p4resetNextLogin').checked };
                if (features & 0x00010000) { r.hint = Q('p4hint').value; }
                meshserver.send(r);
            }
        }

        function p30showDeleteUserDialog() {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', format("Eliminar Usuario {0}", EscapeHtml(currentUser.name)), format("¿Confirmar la eliminación del usuario {0}?", EscapeHtml(currentUser.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Confirmar" + '</label>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p30showDeleteUserDialogEx);
            p10validateDeleteNodeDialog();
        }

        function p30showDeleteUserDialogEx() {
            meshserver.send({ action: 'deleteuser', userid: currentUser._id, username: currentUser.name });
        }

        // Draw device power bars. The bars are 766px wide.
        function drawUserPermissions() {
            var count = 1, x = '';

            // Display common device groups
            var deviceGroupCount = 0, newDeviceGroup = false;
            for (var i in meshes) { if (meshes[i]._id.split('/')[1] != currentUser._id.split('/')[1]) continue; deviceGroupCount++; if ((currentUser.links == null) || (currentUser.links[i] == null)) { newDeviceGroup = true; } }
            if ((deviceGroupCount > 0) && (newDeviceGroup)) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick="return p20showAddMeshUserDialog(1)"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Grupo de Dispositivos" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Grupos de Dispositivos en Común" + '</th><th scope=col></th></tr>';
            if (currentUser.links) {
                var omeshes = [];
                for (var i in currentUser.links) { if (i.startsWith('mesh/')) { if (meshes[i] != null) { omeshes.push(meshes[i]); } } }
                omeshes = getOrderedList(omeshes, 'name');
                for (var i in omeshes) {
                    var cr = 0, mesh = omeshes[i], r = currentUser.links[mesh._id].rights, trash = '', rights = makeDeviceGroupRightsString(r);
                    if (mesh == null) { continue; }
                    if ((userinfo.links) && (userinfo.links[mesh._id] != null) && (userinfo.links[mesh._id].rights != null)) { cr = userinfo.links[mesh._id].rights; }
                    var meshname = '<i>' + "Grupo de Dispositivos Desconocido" + '</i>';
                    if (mesh) { meshname = '<a href=# onclick=\'gotoMesh("' + mesh._id + '");haltEvent(event);\'>' + EscapeHtml(mesh.name) + '</a>'; }
                    if ((currentUser._id != userinfo._id) && ((cr & 2) != 0)) { // 2 = MESHRIGHT_MANAGEUSERS
                        trash = '<a href=# onclick=\'return p30removeMeshFromUser(event,"' + encodeURIComponentEx(mesh._id) + '")\' title="' + "Remover permisos de usuario de este grupo de dispositivos" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                        rights = '<span style=cursor:pointer onclick=p20showAddMeshUserDialog(1,"' + encodeURIComponentEx(mesh._id) + '")>' + rights + ' <img class=hoverButton style=cursor:pointer src=images/link5.png></span>';
                    }
                    x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%><div title="' + "Grupo de Dispositivos" + '" class=m99></div><div>&nbsp;' + meshname + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
                }
            }
            if (count == 1) { x += '<tr><td><div style=padding:6px>&nbsp;<i>' + "No hay grupos de dispositivos en común" + '</i><div></div></div></td><td></td></tr>'; }
            x += '</tbody></table>';

            // Display user groups
            if (usergroups != null) {
                count = 1;
                x += '<br />';
                if ((userinfo.siteadmin & 256) != 0) {
                    var userGroupCount = 0, newUserGroup = false;
                    for (var i in usergroups) {
                        if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != currentUser._id.split('/')[1])) continue;
                        userGroupCount++;
                        if ((currentUser.links == null) || (currentUser.links[i] == null)) { newUserGroup = true; }
                    }
                    if ((userGroupCount > 0) && (newUserGroup)) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick="return p30showAddUserGroupDialog()"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Grupo de Usuarios" + '</button>'; }
                }
                x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Miembro de Grupos de Usuarios" + '</th><th scope=col></th></tr>';
                if (currentUser.links) {
                    var ougroups = [];
                    for (var i in currentUser.links) { if (i.startsWith('ugrp/')) { if (usergroups[i] != null) { ougroups.push(usergroups[i]); } } }
                    ougroups = getOrderedList(ougroups, 'name');
                    for (var i in ougroups) {
                        var group = ougroups[i], r = currentUser.links[ougroups[i]._id].rights, trash = '';
                        var groupname = '<i>' + "Grupo de Usuarios Desconocido" + '</i>';
                        if (group != null) {
                            groupname = EscapeHtml(group.name);
                            if (usergroups != null) { groupname = '<a href=# onclick=\'gotoUserGroup("' + encodeURIComponentEx(ougroups[i]._id) + '");haltEvent(event);\'>' + groupname + '</a>'; }
                        }
                        if ((group.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { trash = '<a href=# onclick=\'return p30RemoveUserGroup(event,"' + encodeURIComponentEx(ougroups[i]._id) + '")\' title="' + "Remover miembro de grupo de usuarios" + '" style=cursor:pointer><i class="fa-solid fa-trash text-danger hoverButton"></i></a>'; }
                        x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td><div title="' + "Grupo de Usuarios" + '" class=m4></div><div>&nbsp;' + groupname + '<div></div></div></td><td><div style=float:right>' + trash + '</div></td></tr>';
                    }
                }
                if (count == 1) { x += '<tr><td><div style=padding:6px>&nbsp;<i>' + "No es miembro de ningún grupo de usuarios" + '</i><div></div></div></td><td></td></tr>'; }
                x += '</tbody></table>';
            }

            // Display common devices
            count = 1;
            x += '<br />';
            if (currentUser._id.split('/')[1] == userinfo._id.split('/')[1]) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick="return p20showAddMeshUserDialog(4)"><i class="fa-solid fa-circle-plus"></i> ' + "Agregar Dispositivo" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Dispositivos en Común" + '</th><th scope=col></th></tr>';
            if (currentUser.links) {
                // Sort the list of devices to display
                var nodelist = [];
                for (var i in currentUser.links) { if (i.startsWith('node/')) { var node = getNodeFromId(i); if (node != null) { nodelist.push(node); } } }
                nodelist.sort(nameSort);
                for (var i in nodelist) {
                    var node = nodelist[i], r = currentUser.links[node._id].rights, trash = '', cr = GetNodeRights(node);
                    if ((userinfo.links) && (userinfo.links[i] != null) && (userinfo.links[i].rights != null)) { cr = userinfo.links[i].rights; }
                    var nodename = node ? EscapeHtml(node.name) : ('<i>' + "Dispositivo Desconocido" + '</i>');
                    if ((cr & 2) != 0) {
                        trash = '<a href=# onclick=\'return p30removeNodeFromUser(event,"' + encodeURIComponentEx(node._id) + '")\' title="' + "Remover permisos de usuario de este grupo de dispositivos" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                        rights = '<span style=cursor:pointer onclick=p20showAddMeshUserDialog(4,"' + encodeURIComponentEx(node._id) + '")>' + makeUserDeviceRightsString(r) + ' <img class=hoverButton style=cursor:pointer src=images/link5.png></span>';
                    }
                    nodename = '<a href=# onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\'>' + nodename + '</a>';
                    x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td style=width:30%><div title="' + "Dispositivo" + '" class=si' + node.icon + '></div><div>&nbsp;' + nodename + '<div></div></div></td><td style=width:70%><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
                }
            }
            if (count == 1) { x += '<tr><td><div style=padding:6px>&nbsp;<i>' + "Sin dispositivos en común" + '</i><div></div></div></td><td></td></tr>'; }
            x += '</tbody></table>';

            QH('p30html2', x);
        }

        function p30removeDeviceSharing(event, nodeid, publicid, guestname) {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', "Eliminar Compartir el Dispostivo", format("¿Confirmar la eliminación del dispositivo compartido \"{0}\"?", decodeURIComponent(guestname)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function (b, tag) { meshserver.send({ action: 'removeDeviceShare', nodeid: tag[0], publicid: tag[1] }); }, 3, [decodeURIComponent(nodeid), decodeURIComponent(publicid)]);
        }

        function p30removeNodeFromUser(event, nodeid) {
            if (xxdialogMode) return;
            var node = getNodeFromId(decodeURIComponent(nodeid));
            setModalContent('xxAddAgent', "Remover Permisos del Dispositivo", format("¿Confirmar la eliminación de los permisos de acceso para el dispositivo \"{0}\"?", EscapeHtml(node.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function (b, node) { meshserver.send({ action: 'adddeviceuser', nodeid: node._id, nodename: node.name, userids: [currentUser._id], rights: 0, remove: true }); }, 3, node);
        }

        function p30removeUserFromNode(event, userid) {
            if (xxdialogMode) return;
            var user = null, name = '';
            userid = decodeURIComponent(userid);
            xxdialogButtons = 3; xxdialogTag = user;
            if (userid.startsWith('user/')) {
                if (users) { user = users[userid]; if (user != null) { name = user.name; } }
                setModalContent('xxAddAgent', "Remover Permisos de Usuario", name ? format("¿Confirmar la eliminación de los permisos de acceso para el usuario \"{0}\"?", name) : "¿Confirmar la eliminación de los permisos de acceso?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function (b, user) { meshserver.send({ action: 'adddeviceuser', nodeid: currentNode._id, nodename: currentNode.name, userids: [userid], rights: 0, remove: true }); });
            } else if (userid.startsWith('ugrp/')) {
                if (usergroups) { user = usergroups[userid]; if (user != null) { name = user.name; } }
                setModalContent('xxAddAgent', "Remover Permisos de Grupo de Usuarios", name ? format("¿Confirmar la eliminación de los permisos de acceso para el grupo de usuarios \"{0}\"?", name) : "¿Confirmar la eliminación de los permisos de acceso?");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function (b, user) {
                    meshserver.send({ action: 'adddeviceuser', nodeid: currentNode._id, nodename: currentNode.name, userids: [userid], rights: 0, remove: true });
                });

            }
        }

        function p30RemoveUserGroup(button, ugrpid) {
            if (xxdialogMode || (usergroups == null)) return;
            var groupid = decodeURIComponent(ugrpid), group = usergroups[groupid];
            var name = (group != null) ? EscapeHtml(group.name) : ('<i>' + "Desconocido" + '</i>');
            setModalContent('xxAddAgent', "Remover Miembro de Grupo de Usuarios", format("¿Confirmar la eliminación del grupo de usuarios \"{0}\"?", name));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p30RemoveUserGroupEx(3, groupid);
            });
        }

        function p30RemoveUserGroupEx(b, groupid) {
            meshserver.send({ action: 'removeuserfromusergroup', ugrpid: groupid, userid: currentUser._id });
        }

        function p30showAddUserGroupDialog() {
            if (xxdialogMode || (usergroups == null)) return;
            var y = '';
            for (var i in usergroups) {
                if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != currentUser._id.split('/')[1])) continue;
                if ((currentUser.links == null) || (currentUser.links[i] == null)) { y += '<option value=' + encodeURIComponentEx(i) + '>' + EscapeHtml(usergroups[i].name) + '</option>'; }
            }
            var x = addHtmlFormFloating("Grupo de Usuarios", '<select id=dp2groupid class="form-select">' + y + '</select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Agregar Miembros", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', p30showAddUserGroupDialogEx);
            Q('dp2groupid').focus();
            return false;
        }

        function p30showAddUserGroupDialogEx() {
            meshserver.send({ action: 'addusertousergroup', ugrpid: decodeURIComponent(Q('dp2groupid').value), usernames: [currentUser._id.split('/')[2]] });
        }

        function p30removeMeshFromUser(e, meshid) {
            if (xxdialogMode) return;
            var mesh = meshes[decodeURIComponent(meshid)];
            if (mesh == null) return;

            setModalContent('xxAddAgent', "Remover Permisos de Grupo de Dispositivos", format("¿Confirmar la eliminación de los permisos de acceso para el grupo de dispositivos \"{0}\"?", EscapeHtml(mesh.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p30removeMeshFromUserEx(3, mesh._id);
            });
        }

        function p30removeMeshFromUserEx(b, meshid) {
            meshserver.send({ action: 'removemeshuser', meshid: meshid, userid: currentUser._id });
        }


        //
        // UserDropDown Menu
        //

        var userDropdownOpen = false;
        var uiSubmenuOpen = false;

        function initializeUserDropdown() {
            var dropdownButton = Q('userDropdownButton');
            var dropdownMenu = Q('userDropdownMenu');
            var uiSettingsButton = document.querySelector('.userDropdownUISettings');
            var uiSubmenu = Q('uiSubmenu');

            if (!dropdownButton || !dropdownMenu) return;

            QS('uiSubmenu').display = 'none';
            QS('uiSubmenu').opacity = '0';
            QS('uiSubmenu').transform = 'translateX(12px)';
            dropdownMenu.classList.remove('show');

                dropdownButton.onclick = function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    userDropdownOpen = !userDropdownOpen;
                    dropdownMenu.classList.toggle('show');
                    if (!userDropdownOpen) {
                        QS('uiSubmenu').display = 'none';
                        QS('uiSubmenu').opacity = '0';
                        QS('uiSubmenu').transform = 'translateX(12px)';
                        uiSubmenuOpen = false;
                        resetChevronArrow();
                    }
                };

            document.addEventListener('click', function(e) {
                var uiSubmenu = Q('uiSubmenu');
                if (!dropdownMenu.contains(e.target) && !dropdownButton.contains(e.target) && !uiSubmenu.contains(e.target)) {
                    dropdownMenu.classList.remove('show');
                    QS('uiSubmenu').display = 'none';
                    QS('uiSubmenu').opacity = '0';
                    QS('uiSubmenu').transform = 'translateX(12px)';
                    userDropdownOpen = false;
                    uiSubmenuOpen = false;
                    resetChevronArrow();
                }
            });
        }

        function toggleUISubmenu() {
            uiSubmenuOpen = !uiSubmenuOpen;

            var uiSubmenu = Q('uiSubmenu');
            var isDesktop = window.innerWidth > 769;
            var reduceMotion = (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);

            if (uiSubmenuOpen) {
                QS('uiSubmenu').display = 'block';

                if (isDesktop && (reduceMotion == false)) {
                    setTimeout(function() {
                        QS('uiSubmenu').opacity = '1';
                        QS('uiSubmenu').transform = 'translateX(0)';
                    }, 10);
                } else {
                    QS('uiSubmenu').opacity = '1';
                    QS('uiSubmenu').transform = 'translateX(0)';
                }
            } else {
                if (isDesktop && (reduceMotion == false)) {
                    QS('uiSubmenu').opacity = '0';
                    QS('uiSubmenu').transform = 'translateX(12px)';
                    setTimeout(function() {
                        QS('uiSubmenu').display = 'none';
                    }, 300);
                } else {
                    QS('uiSubmenu').display = 'none';
                    QS('uiSubmenu').opacity = '0';
                    QS('uiSubmenu').transform = 'translateX(12px)';
                }
            }

            var chevronIcon = document.querySelector('.userDropdownUISettings .fa-chevron-right');
            if (chevronIcon) {
                chevronIcon.style.transform = uiSubmenuOpen ? 'rotate(90deg)' : '';
            }
        }

        function handleUserMenuItem(action) {
            Q('userDropdownMenu').classList.remove('show');
            QS('uiSubmenu').display = 'none';
            QS('uiSubmenu').opacity = '0';
            QS('uiSubmenu').transform = 'translateX(12px)';
            userDropdownOpen = false;
            uiSubmenuOpen = false;
            resetChevronArrow();

            switch(action) {
                case 'devices': goForward('devices'); break;
                case 'events': goForward('events'); break;
                case 'users': goForward('users'); break;
                case 'files': goForward('files'); break;
                case 'server': goForward('server'); break;
                case 'toggle-modern-ui': toggleBootstrapUIMode(); break;
                case 'left-bar': userInterfaceSelectMenu(1); break;
                case 'top-bar': userInterfaceSelectMenu(2); break;
                case 'fixed-width': userInterfaceSelectMenu(3); break;
                case 'toggle-footer': toggleFooterBarMode(); break;
                case 'toggle-night': toggleNightMode(); break;
                case 'notes': showNotes(false); break;
                case 'account': go(2); break;
                case 'logout':
                    if (logoutControls && logoutControls.logoutUrl) {
                        window.location.href = logoutControls.logoutUrl;
                    }
                    break;
            }
        }

        function resetChevronArrow() {
            var arrow = document.querySelector('.userDropdownUISettings .fa-chevron-right');
            if (arrow) arrow.style.transform = '';
        }

        function updateUserDropdownVisibility() {
            var userDropdown = Q('userDropdown');
            var dropdownName = Q('userDropdownName');
            var dropdownImage = Q('userDropdownImage');

            if (logoutControls && logoutControls.name) {
                QS('userDropdown').display = 'block';
                dropdownName.textContent = logoutControls.name;

                if (userinfo && userinfo.flags && (userinfo.flags & 1)) {
                    var rnd = userinfo.accountImageRnd || Math.floor(Math.random() * 9999999999);
                    dropdownImage.src = 'userimage.ashx?rnd=' + rnd;
                }
            } else {
                QS('userDropdown').display = 'none';
            }
        }

        function updateNightModeIcon() {
            var nightModeItem = document.querySelector('.userDropdownMenuItem[data-action="toggle-night"]');
            if (nightModeItem) {
                var icon = nightModeItem.querySelector('#nightModeIcon');
                var textSpan = nightModeItem.querySelector('#nightModeText');
                if (icon && textSpan) {
                    var isNightMode = document.body.classList.contains('night');
                    var iconClass = isNightMode ? 'fa-sun' : 'fa-moon';
                    var text = isNightMode ? "Alternar modo de luz" : "Alternar modo oscuro";
                    var newIcon = document.createElement('i');
                    newIcon.id = 'nightModeIcon';
                    newIcon.className = 'fa ' + iconClass + ' userDropdownMenuIcon night-mode-icon';
                    icon.parentNode.replaceChild(newIcon, icon);
                    textSpan.textContent = text;
                }
            }
        }
        document.addEventListener('DOMContentLoaded', function() {
            initializeUserDropdown();
            updateUserDropdownVisibility();
            updateNightModeIcon();
        });




        //
        // MY USER EVENTS
        //

        var currentUserEvents = null;
        function userEventsUpdate() {
            if (currentUser == null) return;
            var x = '', dateHeader = null;
            for (var i in currentUserEvents) {
                var event = currentUserEvents[i], time = new Date(event.time);
                if (event.msg) {
                    if (event.h == null) { event.h = Math.random(); }
                    if (printDate(time) != dateHeader) {
                        if (dateHeader != null) x += '</table>';
                        dateHeader = printDate(time);
                        x += '<table class="table table-hover p3eventsTable" cellpadding=0 cellspacing=0><tr><td colspan=4 class=DevSt>' + dateHeader + '</td></tr>';
                    }
                    var icon = 'fa-mobile-screen';
                    if (event.etype == 'user') icon = 'fa-user';
                    if (event.etype == 'server') icon = 'fa-server';

                    var msg;
                    if ((event.msgid == null) || (eventsMessageId[event.msgid] == null)) {
                        if (typeof event.msg == 'string') { msg = EscapeHtml(event.msg).split('(R)').join('&reg;'); } else { msg = ''; }
                    } else {
                        msg = eventsMessageId[event.msgid];
                        for (var i in event.msgArgs) {
                            var xx = event.msgArgs[i];
                            if ((typeof xx == 'string') && (xx.indexOf('DATETIME:') == 0)) { xx = printFlexDateTime(new Date(parseInt(xx.substring(9)))); }
                            msg = msg.split('{' + i + '}').join(xx);
                        }
                        //if (event.msgArgs != null) { for (var i in event.msgArgs) { msg = msg.split('{' + i + '}').join(event.msgArgs[i]); } }
                        msg = EscapeHtml(msg).split('(R)').join('&reg;');
                    }
                    if (event.nodeid) {
                        var node = getNodeFromId(event.nodeid);
                        if (node != null) {
                            icon = 'si' + node.icon;
                            msg = '<a href=# onclick=\'gotoDevice("' + event.nodeid + '",10);haltEvent(event);\'>' + EscapeHtml(node.name) + '</a> &rarr; ' + msg;
                        }
                    }
                    if (event.username && (event.username != currentUser.name)) {
                        var guestname = '';
                        if (event.guestname) { guestname = ' / <span title="' + "Esta es una sesión para compartir con invitados" + '">' + EscapeHtml(event.guestname) + '</span>'; }
                        if ((userinfo.siteadmin & 2) && (event.userid)) {
                            msg = '<a href=# onclick=\'gotoUser("' + encodeURIComponentEx(event.userid) + '");haltEvent(event);\'>' + EscapeHtml(event.username) + '</a>' + guestname + ' &rarr; ' + msg;
                        } else {
                            msg = EscapeHtml(event.guestname) + ' &rarr; ' + msg;
                        }
                    } else if (event.guestname) {
                        msg = '<span title="' + "Esta es una sesión para compartir con invitados" + '">' + EscapeHtml(event.guestname) + '</span> &rarr; ' + msg;
                    }
                    if (event.etype == 'relay' || event.action == 'relaylog') icon = 'fa-arrow-right-arrow-left';
                    x += '<tr onclick=showEventDetails(' + event.h + ',3)  onmouseover=eventMouseHover(this,1) onmouseout=eventMouseHover(this,0) style=cursor:pointer><td style=width:18px><i class="fa-solid ' + icon + '"></i></td><td class=g1>&nbsp;</td><td class=style10>' + printTime(time) + ' - ' + msg + '</td></tr><tr></tr>';
                }
            }
            if (dateHeader != null) x += '</table>';
            if (x == '') x = '<br><i>' + "No se Encontraron Eventos" + '</i><br><br>';
            QH('p31events', x);
        }

        function refreshUsersEvents() {
            if (p31filterevents.value != "") {
                meshserver.send({ action: 'events', limit: parseInt(p31limitdropdown.value), userid: currentUser._id, filter: p31filterevents.value });
            } else {
                meshserver.send({ action: 'events', limit: parseInt(p31limitdropdown.value), userid: currentUser._id });
            }
        }


        //
        // Recordings
        //

        var p52recordings = null;
        function updateRecordings() {
            // Save the list of currently checked recordings
            var checkedRecordings = [], elements = document.getElementsByClassName('RecordingCheckbox'), x = '', addHeader = true;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked) { checkedRecordings.push(elements[i].value); } }

            if (p52recordings == null) {
                x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Cargando..." + '</i></div>';
            } else if (typeof p52recordings == 'number') {
                if (p52recordings == 1) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "El servidor no puede acceder la carpeta de grabaciones." + '</i></div>'; }
                else if (p52recordings == 2) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "El servidor no puede obtener las grabaciones de la base de datos." + '</i></div>'; }
                else { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Ha ocurrido un error desconcido." + '</i></div>'; }
            } else if (p52recordings.length == 0) {
                x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "No hay grabaciones." + '</i></div>';
            } else {
                // Display the users using the sorted list
                x += '<table class="table table-hover" cellpadding=0 cellspacing=0><thead class="text-center"><tr>';
                x += '<th>' + "Sesión" + '</th><th style=width:110px>' + nobreak("Tiempo de Inicio") + '</th><th style=width:110px>' + "Duración" + '</th><th style=width:110px>' + "Tamaño" + '</th></tr></thead><tbody>';
                if (p52recordings != null) {
                    var recdate = null;
                    for (var i in p52recordings) {
                        var rec = p52recordings[i], rect = new Date(rec.startTime), day = printDate(rect);
                        if (day != recdate) { recdate = day; x += '<tr><td class=userTableHeader colspan=4>' + day; }
                        x += addRecordingHtml(i, rec);
                    }
                }
                x += '</tbody></table>';
            }
            QH('p52recordings', x);

            // Re-check recordings
            elements = document.getElementsByClassName('RecordingCheckbox');
            for (var i = 0; i < elements.length; i++) { elements[i].checked = ((checkedRecordings.indexOf(elements[i].value) >= 0)); }
            p52updateInfo();
        }

        function addRecordingHtml(i, rec) {
            var sessionLengthStr = '';
            if (rec.lengthTime) { sessionLengthStr = pad2(Math.floor(rec.lengthTime / 3600)) + ':' + pad2(Math.floor((rec.lengthTime % 3600) / 60)) + ':' + pad2(Math.floor(rec.lengthTime % 60)); }
            var sessionStartStr = printTime(new Date(rec.startTime));
            if (rec.sessionStart) { sessionStartStr = printTime(new Date(rec.sessionStart)); }
            var sessionSize = '';
            if (rec.size) { sessionSize = format("{0} Kb", Math.round(rec.size / 1024)); }
            var sessionName = '<i>' + "Desconocido" + '</i>';
            if (rec.name && rec.meshname) {
                var recmesh = meshes[rec.meshid];
                if (recmesh != null) {
                    sessionName = '<a href=# onclick=\'gotoMesh("' + rec.meshid + '")\'>' + EscapeHtml(rec.meshname) + '</a> - <a href=# onclick=\'gotoDevice("' + rec.nodeid + '",10)\'>' + EscapeHtml(rec.name) + '</a>';
                } else {
                    sessionName = EscapeHtml(rec.meshname) + ' - ' + EscapeHtml(rec.name);
                }
            }
            if ((rec.userids != null) && (rec.userids.length > 0)) {
                if (rec.userids.length > 1) {
                    sessionName += ' - ' + format('{0} users', rec.userids.length);
                } else {
                    var ruser = null;
                    if (users != null) { ruser = users[rec.userids[0]]; }
                    if (ruser != null) {
                        sessionName += ' - <a href=# onclick=\'gotoUser("' + encodeURIComponentEx(rec.userids[0]) + '")\'>' + EscapeHtml(ruser.name) + '</a>';
                    } else {
                        sessionName += ' - ' + EscapeHtml(rec.userids[0].split('/')[2]);
                    }
                }
            }

            if (rec.protocol == 1) { sessionName += ' - ' + "Sesión de la Terminal"; }
            if (rec.protocol == 2) { sessionName += ' - ' + "Sesión de Escritorio"; }
            if (rec.protocol == 5) { sessionName += ' - ' + "Transferencia de Archivos"; }
            if (rec.protocol == 6) { sessionName += ' - ' + "Administrar Powershell"; }
            if (rec.protocol == 8) { sessionName += ' - ' + "Shell del Usuario"; }
            if (rec.protocol == 9) { sessionName += ' - ' + "Usuario PowerShell"; }
            if (rec.protocol == 100) { sessionName += ' - ' + "Intel&reg; AMT WSMAN"; }
            if (rec.protocol == 101) { sessionName += ' - ' + "Redirección de Intel&reg; AMT"; }
            if (rec.protocol == 200) { sessionName += ' - ' + "Mensajero"; }

            var actions = '', icon = 'fa-circle-xmark text-danger';
            if (rec.present == 1) {
                icon = 'fa-circle-check text-success';
                actions = '<div style=cursor:pointer;float:right><a onclick=downloadFile("recordings.ashx?file=' + encodeURIComponentEx(rec.filename) + '")><i class="fa-fw fa-solid fa-download fa-sm" title="Download Recording"></i></a>&nbsp;</div>';
                actions += '<div style=cursor:pointer;float:right><a href="player.htm?stream=' + encodeURIComponentEx(rec.filename) + '") rel=\"noreferrer noopener\" target=\"mcplay-' + encodeURIComponentEx(rec.filename) + '\"><i class="fa-fw fa-solid fa-play fa-sm" title="Play Recording"></i></a>&nbsp;</div>';
            }
            var x = '<tr tabindex=0 onmouseover=userMouseHover2(this,1) onmouseout=userMouseHover2(this,0) onkeypress="if (event.key==\'Enter\') showRecordingDialog(event,\'' + i + '\')"><td style=cursor:pointer>';
            x += '<div class=bar style=width:100%>';
            //x += '<div class=baricon><input class=RecordingCheckbox value="' + encodeURIComponentEx(rec.filename) + '" onclick=p52updateInfo() type=checkbox></div>';
            x += '<div></div>';
            x += '<div class=baricon onclick=showRecordingDialog(event,"' + i + '")><i class="fa-fw fa-solid ' + icon + '"></i></div>';
            x += '<div class=g1 onclick=showRecordingDialog(event,"' + i + '")></div><div class=g2 onclick=showRecordingDialog("' + i + '")></div>';
            x += '<div style=line-height:24px onclick=showRecordingDialog(event,"' + i + '")>' + actions + '<div style=font-size:16px>' + sessionName + '</div></div></div><td style=text-align:center>' + sessionStartStr + '<td style=text-align:center>' + sessionLengthStr + '<td style=text-align:center>' + sessionSize;
            return x;
        }

        function showRecordingDialog(event, i) {
            if (xxdialogMode) return;
            if ((event.target.tagName == 'IMG') || (event.target.tagName == 'A')) return;
            var rec = p52recordings[i];
            var x = '';
            if (rec.protocol) {
                var protocolStr = "Desconocido";
                if (rec.protocol == 1) { protocolStr = "Terminal"; }
                if (rec.protocol == 2) { protocolStr = "Escritorio"; }
                if (rec.protocol == 5) { protocolStr = "Archivos"; }
                if (rec.protocol == 6) { protocolStr = "Administrar Powershell"; }
                if (rec.protocol == 8) { protocolStr = "Shell del Usuario"; }
                if (rec.protocol == 9) { protocolStr = "Usuario PowerShell"; }
                if (rec.protocol == 100) { protocolStr = "Intel&reg; AMT WSMAN"; }
                if (rec.protocol == 101) { protocolStr = "Redirección de Intel&reg; AMT"; }
                if (rec.protocol == 200) { protocolStr = "Mensajero"; }
                x += addHtmlValue4("Protocolo", protocolStr);
            }
            x += addHtmlValue4("Estado", (rec.present == 1) ? "Presente en el servidor" : "No está en el servidor");
            if (rec.name) { x += addHtmlValue4("Nombre del Dispositivo", EscapeHtml(rec.name)); }
            if (rec.meshname) { x += addHtmlValue4("Grupo de Dispositivos", EscapeHtml(rec.meshname)); }
            if (rec.size) { x += addHtmlValue4("Tamaño", format("{0} bytes", rec.size)); }
            if (rec.startTime) { x += addHtmlValue4("Tiempo de Inicio", printTime(new Date(rec.startTime))); }
            if (rec.startTime && rec.lengthTime) { x += addHtmlValue4("Tiempo de Finalización", printTime(new Date(rec.startTime + (rec.lengthTime * 1000)))); }
            if (rec.lengthTime) { x += addHtmlValue4("Duración", pad2(Math.floor(rec.lengthTime / 3600)) + ':' + pad2(Math.floor((rec.lengthTime % 3600) / 60)) + ':' + pad2(Math.floor(rec.lengthTime % 60))); }
            if (rec.multiplex == true) { x += addHtmlValue4("Multiplexor", "Habilitado"); }
            if (rec.userids) { for (var i in rec.userids) { x += addHtmlValue4("Usuario", rec.userids[i].split('/')[2]); } }

            xxdialogButtons = 9;
            setModalContent('xxAddAgent', "Detalles de la Grabación", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', null);
        }

        function refreshRecodings() { meshserver.send({ action: 'recordings', limit: 1000 }); }
        function openRecodringPlayer() { if (!xxdialogMode) safeNewWindow(window.location.origin + '{{{domainurl}}}player.htm', 'meshcentral-deskplayer'); }
        function p52updateInfo() {
            var elements = document.getElementsByClassName('RecordingCheckbox'), checkcount = 0;
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { checkcount++; } }
            if (checkcount > 0) {

            } else {

            }
        }

        //
        // FILE SELECTOR, DIALOG 3
        //

        function d3init() {
            d3fileoptions = { dialog: 1, filter: 'd3filter', files: 'd3serverfiles', folderup: 'p3FolderUp', currentFolder: 'p3CurrentFolder', func: d3setActions };
            Q('d3localFile').value = '';
            Q('d3localFile').accept = Q('d3filter').value;
            d3modechange();
        }

        function d3modechange() {
            var mode = Q('d3uploadMode').value;
            QV('d3localmode', mode == 1);
            QV('d3servermode', mode == 2);
            if (mode == 1) { d3setActions(); } else { d3updatefiles(); }
        }

        var d3filetreelinkpath;
        var d3filetreelocation = [];
        var d3fileoptions = null;
        function d3updatefiles() {
            if (d3fileoptions == null) return;
            if ((d3fileoptions.filter == 'd3filter') && (Q('d3uploadMode').value == 1)) return;
            var html1 = '', html2 = '', filetreex = filetree, folderdepth = 1, publicPath = null, lastFolderName = '';

            // Navigate to path location, build the paths at the same time
            var d3filetreelocation2 = [], oldlinkpath = d3filetreelinkpath, checkedBoxes = [], checkboxes = document.getElementsByName('fc');
            for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { checkedBoxes.push(checkboxes[i].value) }; } // Save all existing checked boxes

            d3filetreelinkpath = '';
            for (var i in d3filetreelocation) {
                if ((filetreex.f != null) && (filetreex.f[d3filetreelocation[i]] != null)) {
                    d3filetreelocation2.push(d3filetreelocation[i]);
                    if ((folderdepth == 1)) {
                        var sp = d3filetreelocation[i].split('/');
                        publicPath = window.location.origin + domainUrl + sp[0] + 'files/' + sp[2];
                        if (d3filetreelocation[i] === userinfo._id) { d3filetreelinkpath += 'self'; } else { d3filetreelinkpath += (sp[0] + '/' + sp[2]); }
                    } else {
                        if (d3filetreelinkpath != '') { d3filetreelinkpath += '/' + d3filetreelocation[i]; if (folderdepth > 2) { publicPath += '/' + d3filetreelocation[i]; } }
                    }
                    filetreex = filetreex.f[d3filetreelocation[i]];
                    lastFolderName = filetreex.n;
                    folderdepth++;
                } else {
                    break;
                }
            }
            d3filetreelocation = d3filetreelocation2; // In case we could not go down the full path, we set the new path location here.

            // Sort the files
            var filetreexx = p5sort_files(filetreex.f);

            // File filter
            var fileFilter = '';
            if (d3fileoptions.filter) { fileFilter = Q(d3fileoptions.filter).value };

            // Display all files and folders at this location
            for (var i in filetreexx) {
                // Figure out the name and shortname
                var f = filetreexx[i], name = f.n, shortname;

                // Filter out files
                if ((f.t == 3) && (fileFilter != '') && (f.nx.toLowerCase().endsWith(fileFilter) == false)) { continue; }
                // if (name.length > 70) { shortname = '<span title="' + EscapeHtml(name) + '">' + EscapeHtml(name.substring(0, 70)) + ("..." + '</span>'); } else { shortname = EscapeHtml(name); }
                // Removed redundant filename length check because we handle it in the CSS
                shortname = EscapeHtml(name);

                // Figure out the size
                var fsize = '';
                if (f.s != null) { fsize = getFileSizeStr(f.s); }

                var h = '';
                if (f.t != 3) {
                    var title = '';
                    h = '<div class=filelist file=999><span style=float:right title="' + title + '"></span><span title="' + shortname + '"><div class=fileIcon' + f.t + ' onclick=d3folderset("' + encodeURIComponentEx(f.nx) + '")></div>&nbsp;<a href=# style=cursor:pointer onclick=\'return d3folderset("' + encodeURIComponentEx(f.nx) + '")\'>' + shortname + '</a></span></div>';
                } else {
                    var link = shortname;
                    //if (f.s > 0) { link = "<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"downloadfile.ashx?link=" + encodeURIComponentEx(filetreelinkpath + '/' + f.nx) + "\">" + shortname + "</a>"; }
                    h = '<div class=filelist file=3><input style=float:left name=fcx class="fcb form-check-input me-2" type=checkbox onchange=d3setActions() value="' + f.nx + '">&nbsp;<span style=float:right>' + EscapeHtml(fsize) + '</span><span title="' + shortname + '"><div class=fileIcon' + f.t + '></div>' + link + '</span></div>';
                }

                if (f.t < 3) { html1 += h; } else { html2 += h; }
            }

            if (d3fileoptions.currentFolder) { QH(d3fileoptions.currentFolder, lastFolderName); }
            QH(d3fileoptions.files, html1 + html2);
            QE(d3fileoptions.folderup, d3filetreelocation.length > 0);
            if (d3fileoptions.func) { d3fileoptions.func(); }
        }

        function d3folderset(x) { d3filetreelocation.push(decodeURIComponent(x)); d3updatefiles(); return false; }
        function d3folderup(x) { if (x == null) { d3filetreelocation.pop(); } else { while (d3filetreelocation.length > x) { d3filetreelocation.pop(); } } d3updatefiles(); }
        function d3getFileSel() { var cc = []; var checkboxes = document.getElementsByName('fcx'); for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { cc.push(checkboxes[i].value) } } return cc; }
        function d3setActions() {
            if (d3fileoptions.dialog == 1) {
                var mode = Q('d3uploadMode').value;
                if (mode == 1) {
                    QE('idx_dlgOkButton', Q('d3localFile').value.length > 0);
                } else {
                    QE('idx_dlgOkButton', d3getFileSel().length == 1);
                }
            } else if (d3fileoptions.dialog == 2) {
                QE('idx_dlgOkButton', d3getFileSel().length == 1);
            }
        }


        //
        // MY REPORTS
        //

        var reportCSV = null;

        function generateReportDialog() {
            if (xxdialogMode) return;
            var y = '', x = '', settings = JSON.parse(getstore('_ReportSettings', '{}'));

            var options = { 1: "Sesiones Remotas", 2: "Uso de tráfico de usuario", 3: "Inicios de sesión de usuario" }
            if (userinfo.siteadmin == 0xFFFFFFFF) { options[4] = "Registro de Base de datos"; }
            for (var i in options) { y += '<option value=' + i + ((settings.type == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Tipo", '<select id=d2reportType class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');

            x += '<div id=d2GroupByDiv style=display:none>';
            y = '';
            var options = { 1: "Usuario", 2: "Dispositivo", 3: "Día" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Agrupar Por", '<select id=d2groupBy class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2GroupByDiv2 style=display:none>';
            y = '';
            var options = { 1: "Usuario", 3: "Día" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy2 == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Agrupar Por", '<select id=d2groupBy2 class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2DeviceGroupDiv style=display:none>';
            y = '<option value=0' + ((settings.devGroup == 0) ? ' selected' : '') + '>' + "Todos" + '</option>';
            var omeshs = getOrderedList(meshes, 'name');
            for (var i in omeshs) { y += '<option value=' + encodeURIComponentEx(omeshs[i]._id) + ((settings.devGroup == omeshs[i]._id) ? ' selected' : '') + '>' + EscapeHtml(omeshs[i].name) + '</option>'; }
            x += addHtmlFormFloating("Grupo de Dispositivos", '<select onchange=generateReportDialogValidate() id=d2groupId class="form-select">' + y + '</select>');
            x += '</div>';

            x += '<div id=d2timeBackDiv style=display:none>';
            y = '';
            if (settings.timeRange == null) { settings.timeRange = 1; }
            var options = { 1: "Último día", 7: "Últimos 7 días", 30: "Últimos 30 días", 0: "Intervalo de tiempo" }
            for (var i in options) { y += '<option value=' + i + ((settings.timeRange == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Tiempo", '<select id=d2timeRange class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2timeRangeDiv style=display:none>';
            x += addHtmlFormFloating("Inicio de rango de tiempo", '<input id=d2timeRangeStartSelector class="flatpickr form-control" type="text" placeholder="' + "Seleccionad fecha y hora..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Final del rango de tiempo", '<input id=d2timeRangeEndSelector class="flatpickr form-control" type="text" placeholder="' + "Seleccionad fecha y hora..." + '" data-id="altinput">');
            x += '</div>';

            x += '<div id=d2showTrafficDiv style=display:none>';
            x += addHtmlValue("", '<div style=width:250px><label><input type=checkbox class="form-check-input me-2" id=d2showTraffic ' + ((settings.showTraffic) ? 'checked' : '') + '>' + "Mostrar tráfico" + '</label><div>');
            x += '</div>';

            setModalContent('xxAddAgent', "Generar informe", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => generateReportDialogEx('', xxdialogTag));
            generateReportDialogValidate();

            var lastWeek = new Date();
            lastWeek.setDate(lastWeek.getDate() - 7);
            var rangeStartTime = flatpickr('#d2timeRangeStartSelector', {
                enableTime: true, maxDate: new Date(), defaultDate: lastWeek, minuteIncrement: 1, plugins: [new confirmDatePlugin({})],
                onChange: function(selectedDates, dateStr, instance) {
                    rangeEndTime.set('minDate', dateStr);
                    if (rangeEndTime.selectedDates[0] && rangeEndTime.selectedDates[0] < selectedDates[0]) {
                        rangeEndTime.clear();
                    }
                }
            });
            var rangeEndTime = flatpickr('#d2timeRangeEndSelector', { enableTime: true, maxDate: new Date(), defaultDate: new Date(), minuteIncrement: 1, plugins: [new confirmDatePlugin({})] });
            xxdialogTag = { rangeStart: rangeStartTime, rangeEnd: rangeEndTime };
        }

        function generateReportDialogValidate() {
            QV('d2timeRangeDiv', Q('d2timeRange').value == 0);
            QV('d2timeBackDiv', Q('d2reportType').value != 4);
            QV('d2GroupByDiv', Q('d2reportType').value == 1);
            QV('d2GroupByDiv2', Q('d2reportType').value == 3);
            QV('d2DeviceGroupDiv', Q('d2reportType').value == 1);
            QV('d2showTrafficDiv', Q('d2reportType').value == 1);
        }

        function generateReportDialogEx(b, tag) {
            var start, end;
            if (Q('d2timeRange').value == 0) {
                end = Math.floor(tag.rangeEnd.selectedDates[0].getTime() / 1000);
                start = Math.floor(tag.rangeStart.selectedDates[0].getTime() / 1000);
            } else {
                end = Math.floor(new Date() / 1000);
                start = new Date();
                start = Math.floor(start.setDate(start.getDate() - Q('d2timeRange').value) / 1000);
            }
            var tz = null;
            try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (ex) { }
            var devGroup = decodeURIComponent(Q('d2groupId').value);
            if (devGroup == 0) { devGroup = null; }
            putstore('_ReportSettings', JSON.stringify({ type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), groupBy2: parseInt(Q('d2groupBy2').value), timeRange: parseInt(Q('d2timeRange').value), devGroup: devGroup, showTraffic: Q('d2showTraffic').checked }));
            var groupBy = parseInt(Q('d2groupBy').value);
            if (Q('d2reportType').value == 3) { groupBy = parseInt(Q('d2groupBy2').value); }
            meshserver.send({ action: 'report', type: parseInt(Q('d2reportType').value), groupBy: groupBy, start: start, end: end, tz: tz, tf: new Date().getTimezoneOffset(), l: getLang(), devGroup: devGroup, showTraffic: Q('d2showTraffic').checked });
        }

        function renderReport(r) {
            var colTranslation = { time: "Tiempo", device: "Dispositivo", session: "Sesión", user: "Usuario", devgroup: "Grupo de Dispositivos", guest: "Invitado", length: "Longitud", bytesin: "Bytes entrantes", bytesout: "Bytes salientes", os: "Sistema Operativo", ip: "Dirección IP", browser: "Navegador", msg: "Mensaje", twofactor: "2do factor" }
            var x = '<table class="table table-hover">', firstItem;
            var sumByCol = null; // Indicate by what colum we sum by
            var sumByValues = []; // Indicate by what values we sum by

            if (Object.keys(r.groups).length == 0) {
                reportCSV = null;
                QV('p60downloadReportDiv', false);
                x += '<tr><td style=text-align:center;padding-top:32px>' + "El informe no ha devuelto contenido." + '</tr></td>';
            } else {
                reportCSV = '"' + "Grupo" + '"';
                x += '<tr>'
                for (var i in r.columns) {
                    var coltitle;
                    if (colTranslation[r.columns[i].title] != null) { coltitle = colTranslation[r.columns[i].title]; } else { coltitle = EscapeHtml(r.columns[i].title); }
                    var style = '';
                    if (r.columns[i].align) { style += ';text-align:' + EscapeHtml(r.columns[i].align); } else { style += ';text-align:left'; }
                    if ((i == 0) && ((r.columns[i].format == 'datetime') || (r.columns[i].format == 'time'))) { style += ';width:1%'; }
                    x += '<th style=' + style + '>' + coltitle + '</th>';
                    reportCSV += ',"' + csvClean(coltitle) + '"';
                    firstItem = false;
                }
                x += '</tr>'
                reportCSV += '\r\n';
                for (var i in r.groups) {
                    x += '<tr style=height:8px></tr>'
                    x += '<tr><td colspan=' + r.columns.length + ' style="border-bottom:1pt solid black"><b>'
                    if (i != 0) { x += renderReportFormat(i, r.groupFormat); }
                    x += '</b></td></tr>'
                    for (var j in r.groups[i].entries) {
                        var e = r.groups[i].entries[j];
                        x += '<tr>'
                        if (i != 0) { reportCSV += '"' + csvClean(renderReportFormatCSV(i, r.groupFormat)) + '"'; } else { reportCSV += '""'; }
                        for (var k in r.columns) {
                            var style = '';
                            if (r.columns[k].format != null) { style = 'white-space:nowrap;' }
                            if (r.columns[k].align) { style = 'text-align:' + EscapeHtml(r.columns[k].align); }
                            if (e[r.columns[k].id] != null) {
                                x += '<td style="' + style + '">' + renderReportFormat(e[r.columns[k].id], r.columns[k].format) + '</td>';
                                reportCSV += ',"' + csvClean(renderReportFormatCSV(e[r.columns[k].id], r.columns[k].format)) + '"';
                            } else {
                                x += '<td></td>';
                                reportCSV += ',';
                            }
                            if (r.columns[k].sumBy != null) {
                                var v1 = e[r.columns[k].sumBy];
                                if (r.columns[k].sumBy === true) { v1 = true; }
                                var v2 = e[r.columns[k].id];
                                sumByCol = r.columns[k].sumBy;
                                if (v2 != null) {
                                    if (sumByValues.indexOf(v1) == -1) { sumByValues.push(v1); }
                                    if (r.columns[k].subtotals == null) { r.columns[k].subtotals = {}; r.columns[k].totals = {}; }
                                    if (r.columns[k].subtotals[v1] == null) { r.columns[k].subtotals[v1] = v2; } else { r.columns[k].subtotals[v1] += v2; }
                                    if (r.columns[k].totals[v1] == null) { r.columns[k].totals[v1] = v2; } else { r.columns[k].totals[v1] += v2; }
                                }
                            }
                        }
                        x += '</tr>'
                        reportCSV += '\r\n';
                    }
                }

                // Display totals
                if (sumByCol != null) {
                    var sumByColNum = -1;
                    if (sumByCol === true) { sumByColNum = 99999; } else { for (var i in r.columns) { if (r.columns[i].id == sumByCol) { sumByColNum = i; } } }
                    if (sumByColNum >= 0) {
                        sumByValues.sort();
                        var firstRow = true;
                        x += '<tr style=height:8px></tr>'
                        for (var j in sumByValues) {
                            x += '<tr>'
                            for (var i in r.columns) {
                                if (i == sumByColNum) {
                                    var style = '';
                                    if (r.columns[k].align) { style += ';text-align:' + EscapeHtml(r.columns[k].align); }
                                    if (firstRow) { style += ';border-top:1pt solid #777'; }
                                    x += '<td style="color:#777' + style + '">' + renderReportFormat(sumByValues[j], r.columns[i].format); + '</td>';
                                } else if (r.columns[i].totals != null) {
                                    var style = '';
                                    if (r.columns[k].align) { style += ';text-align:' + EscapeHtml(r.columns[k].align); }
                                    if (firstRow) { style += ';border-top:1pt solid #777'; }
                                    x += '<td style="color:#777' + style + '">' + renderReportFormat(r.columns[i].totals[sumByValues[j]], r.columns[i].format); + '</td>';
                                } else {
                                    x += '<td></td>';
                                }
                            }
                            x += '</tr>'
                            firstRow = false;
                        }
                    }
                }
                QV('p60downloadReportDiv', true);
            }
            x += '</table>';
            QH('p60report', x);
        }

        function renderReportFormat(v, f) {
            if (v == null) return '';
            if (f == 'datetime') { return printDateTime(new Date(v)); }
            if (f == 'time') { return printTime(new Date(v)); }
            if (f == 'protocol') {
                if (v == 1) return "Terminal";
                if (v == 2) return "Escritorio";
                if (v == 5) return "Archivos";
                if (v == 6) return "Administrar Powershell";
                if (v == 8) return "Shell del Usuario";
                if (v == 9) return "Usuario PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "Mensajero";
                if (v == 201) return "Web-RDP";
                if (v == 202) return "Web-SSH";
                if (v == 203) return "Web-SFTP";
                if (v == 204) return "Web-VNC";
                return "Desconocido" + ' (' + v + ')';
            }
            if (f == 'seconds') {
                var seconds = v % 60;
                var minutes = Math.floor(v / 60) % 60;
                var hours = Math.floor(v / 3600);
                return zeroPad(hours, 2) + ':' + zeroPad(minutes, 2) + ':' + zeroPad(seconds, 2);
            }
            if (f == 'node') {
                var node = getNodeFromId(v);
                if (node != null) { return '<div onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px class="j' + node.icon + '"></div>' + EscapeHtml(node.name); } else { return '<i>' + "Desconocido" + '</i>'; }
            }
            if (f == 'mesh') {
                var mesh = meshes[v];
                if (mesh != null) { return '<div onclick=\'gotoMesh("' + mesh._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px;cursor:pointer class="m99"></div>' + EscapeHtml(mesh.name); } else { return '<i>' + "Desconocido" + '</i>'; }
            }
            if (f == 'nodemesh') {
                var nodeid = null, meshid = null;
                var s = v.split('/'), x = '<table><tr>';
                if (s.length >= 3) { nodeid = s[0] + '/' + s[1] + '/' + s[2]; }
                if (s.length >= 6) { meshid = s[3] + '/' + s[4] + '/' + s[5]; }
                if (meshid != null) {
                    var mesh = meshes[meshid];
                    if (mesh != null) { x += '<td><div onclick=\'gotoMesh("' + mesh._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px;cursor:pointer class="m99"></div>' + EscapeHtml(mesh.name) + '&nbsp;&nbsp;&nbsp;</td>'; } else { return '<i>' + "Desconocido" + '</i>'; }
                }
                var node = getNodeFromId(nodeid);
                if (node != null) { x += '<td><div onclick=\'gotoDevice("' + node._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px class="j' + node.icon + '"></div>' + EscapeHtml(node.name) + '</td>'; } else { return '<i>' + "Desconocido" + '</i>'; }
                x += '</tr></table>';
                return x;
            }
            if (f == 'user') {
                var user = null;
                if (v == userinfo._id) { user = userinfo; } else { if (users != null) { user = users[v]; } }
                if (user != null) {
                    var name = user.name;
                    if (user.realname != null) { name += ', ' + user.realname; }
                    return '<div onclick=\'gotoUser("' + user._id + '",10);haltEvent(event);\' style=float:left;margin-right:4px;cursor:pointer><i class="fa-solid fa-user-circle"></i></div>' + EscapeHtml(name);
                } else {
                    return '<i>' + "Usuario desconocido" + '</i>';
                }
            }
            if (f == 'msg') {
                if (v == 107) return '<div style=float:left;margin-right:4px;cursor:pointer><i class="fa-solid fa-check-circle text-success"></i></div>' + "Inicio de sesión exitoso";
                if (v == 108) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny2"></div>' + "Segundo factor incorrecto";
                if (v == 109) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny4"></div>' + "Cuenta bloqueada";
                if (v == 110) return '<div style=float:left;margin-right:4px;cursor:pointer><i class="fa-solid fa-exclamation-triangle text-warning"></i></div>' + "Intento de inicio de sesión no válido";
            }
            if (f == '2fa') {
                if (v == '') { return "Ninguno"; }
                if (v == 'backup') { return "Códigos de Respaldo"; }
                if (v == 'fido') { return "tecla FIDO"; }
                if (v == 'sms') { return "mensaje de texto"; }
                if (v == 'hwotp') { return "OTP de hardware"; }
                if (v == 'messenger') { return "Mensajería"; }
                if (v == 'push') { return "Notificación de inserción"; }
                if (v == 'otp') { return "Contraseña de un solo uso"; }
                if (v == 'cookie') { return "Recordar dispositivo"; }
                if (v == 'tokenlogin') { return "Ficha de inicio de sesión"; }
                if (v == 'ipaddr') { return "Dirección IP"; }
                if (v == 'sso') { return "Inicio de sesión único"; }
            }
            if (f == 'records') {
                var c = {
                    'node': "Registro de dispotitivos",
                    'mesh': "Registro de grupo de dispositivos",
                    'user': "Registros de Usuario",
                    'sysinfo': "Registro de informacion de dispossitivos",
                    'iploc': "Registros de Ubicacion IP",
                    'note': "Registro de Notas",
                    'ifinfo': "Registros de información de la interfaz de red.",
                    'cfile': "Registro de archivos de configuracion",
                    'lastconnect': "Registros de hora de última conexión",
                    'power': "Registro de cambios en origen de poder",
                    'events': "Registro de eventos",
                    'serverstats': "Historial de estadisticas del servidor",
                    'ugrp': "Registro de grupos de usuario",
                    'deviceshare': "Registros de uso compartido de dispositivos",
                    'logintoken': "Registro de Tokens de inicio de sesion",
                    'smbios': "Registro SMBIOS del dispositivo",
                    'pmt': "Registro de Notificaciones Push del dispositivo",
                    'meshcentral': "Dispositivo, usuarios, grupos y otros registros"
                }
                return (c[v] != null) ? c[v] : v;
            }
            return EscapeHtml(v);
        }

        function renderReportFormatCSV(v, f) {
            if (f == 'datetime') { return printDateTime(new Date(v)); }
            if (f == 'time') { return printTime(new Date(v)); }
            if (f == 'protocol') {
                if (v == 1) return "Terminal";
                if (v == 2) return "Escritorio";
                if (v == 5) return "Archivos";
                if (v == 6) return "Administrar Powershell";
                if (v == 8) return "Shell del Usuario";
                if (v == 9) return "Usuario PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "Mensajero";
                if (v == 201) return "Web-RDP";
                if (v == 202) return "Web-SSH";
                if (v == 203) return "Web-SFTP";
                if (v == 204) return "Web-VNC";
                return "Desconocido" + ' (' + v + ')';
            }
            if (f == 'node') {
                var node = getNodeFromId(v);
                if (node != null) { return node.name; }
            }
            if (f == 'mesh') {
                var mesh = meshes[v];
                if (mesh != null) { return mesh.name }
            }
            if (f == 'user') {
                var user = null;
                if (v == userinfo._id) { user = userinfo; } else { if (users != null) { user = users[v]; } }
                if (user != null) {
                    var name = user.name;
                    if (user.realname != null) { name += ' - ' + user.realname; }
                    return name;
                } else {
                    return "Usuario desconocido";
                }
            }
            if (f == 'msg') {
                if (v == 107) return "Inicio de sesión exitoso";
                if (v == 108) return "Segundo factor incorrecto";
                if (v == 109) return "Cuenta bloqueada";
                if (v == 110) return "Intento de inicio de sesión no válido";
            }
            if (f == 'records') { return renderReportFormat(v, f); }
            return '' + v;
        }

        function p60downloadReport() {
            if (xxdialogMode || (reportCSV == null)) return;
            saveAs(stringToUtf8Blob(reportCSV), "Report.csv");
        }

        //
        // NOTIFICATIONS
        //

        var notifications = [];

        // Toggle showing notifications
        function clickNotificationIcon(show) {
            //addNotification({ icon:0, text:'test' });
            if (show == true) { QV('notifiyBox', true); } else if (show == false) { QV('notifiyBox', false); } else { QV('notifiyBox', QS('notifiyBox')['display'] == 'none'); }
            drawNotifications();
        }

        // Set the notification count on the upper right of the screen
        function setNotificationCount(c) {
            var badge = Q('notificationBadge');
            if (parseInt(badge.innerHTML) === c) return;
            badge.innerHTML = c;
            QV('notificationCount', c > 0);
        }

        // Close notification box when clicking outside
        function setupNotificationClickOutside() {
            document.addEventListener('click', function(event) {
                var notificationBox = Q('notifiyBox');
                var notificationIcon = Q('notificationCount');
                if (notificationBox && QS('notifiyBox')['display'] !== 'none') {
                    if (!notificationBox.contains(event.target) && !notificationIcon.contains(event.target)) {
                        QV('notifiyBox', false);
                    }
                }
            });
        }
        setupNotificationClickOutside();


        // Refresh the notification box
        function drawNotifications() {
            var notifySettings = getstore('notifications', 0);
            var r = '';
            if (notifications.length == 0) {
                r = '<div style=margin:5px>' + "Actualmente no hay notificaciones" + '</div>';
            } else {
                for (var i in notifications) {
                    var n = notifications[i], t = '', d = new Date(n.time), icon = 0;
                    if (n.title != null) { t = '<b>' + EscapeHtml(n.title) + '</b>: ' }
                    if (n.nodeid != null) {
                        var node = getNodeFromId(n.nodeid);
                        if (node != null) {
                            icon = node.icon;
                            if (notifySettings & 16) { t = '<b>' + EscapeHtml(meshes[node.meshid].name) + ' / ' + EscapeHtml(node.name) + '</b>: '; } else { t = '<b>' + EscapeHtml(node.name) + '</b>: '; } // Display with or without group name
                        }
                    }

                    r += '<div title="' + format("Ocurrió en {0}", printDateTime(d)) + '" id="notifyx' + n.id + '" class=notification style="cursor:pointer;' + (notifications.length > 1 ? 'border-top:1px solid orange' : '') + '">';
                    if (icon) { r += '<div class=j' + icon + ' onclick="notificationSelected(' + n.id + ')" style=margin:5px;float:left></div>'; }
                    r += '<div onclick="notificationDelete(' + n.id + ')" class=unselectable title="' + "Borrar esta notificación" + '" style=margin:5px;float:right;color:orange><b>X</b></div><div onclick="notificationSelected(' + n.id + ')" style=margin:5px>' + t + EscapeHtml(n.text) + '</div><div style=margin-left:5px;margin-bottom:5px;color:gray;font-size:10px>' + printDateTime(d) + '</div></div>';
                }
            }
            var deleteall = '';
            if (notifications.length > 1) { deleteall = '<div id="notifyRemoveAll" onclick="deleteAllNotifications()" style="cursor:pointer;margin:5px;color:orange;text-align:right;padding-right:3px">' + "Borrar todo" + '</div>'; }
            QH('notifiyBox', '<div class=customScroll style="max-height:170px;overflow-y:auto;margin:5px">' + deleteall + r + '</div>');
        }

        // A notification was selected
        function notificationSelected(id, del) {
            var j = -1;
            for (var i in notifications) { if (notifications[i].id == id) { j = i; } }
            if (j != -1) {
                notificationSelectedEx(notifications[j], id);
                if (del && notifications[j]) {
                    if (notifications[j].notification) { notifications[j].notification.close(); delete notifications[j].notification; }
                    notificationDelete(id);
                }
            }
        }

        function notificationSelectedEx(n, id) {
            if (n.nodeid != null) {
                if (n.tag == 'desktop') gotoDevice(n.nodeid, 12); // Desktop
                else if (n.tag == 'terminal') gotoDevice(n.nodeid, 11); // Terminal
                else if (n.tag == 'files') gotoDevice(n.nodeid, 13); // Files
                else if (n.tag == 'intelamt') gotoDevice(n.nodeid, 14); // Intel AMT
                else if (n.tag == 'console') gotoDevice(n.nodeid, 15); // Files
                else gotoDevice(n.nodeid, 10); // General
            } else {
                if ((n.tag == 'backupcodes') && !xxdialogMode) { go(2); account_manageOtp(0); notificationDelete(id); } // 2FA backup codes
                else if ((n.tag != null) && n.tag.startsWith('meshmessenger/')) {
                    safeNewWindow('/messenger?id=' + n.tag + '&title=' + encodeURIComponentEx(n.username), n.tag.split('/')[2]);
                    notificationDelete(id);
                } else if (n.url != null) {
                    safeNewWindow(n.url);
                    notificationDelete(id);
                }
            }
        }

        // Remove one notification
        function notificationDelete(id) {
            var j = -1, e = Q('notifyx' + id);
            if (e != null) {
                for (var i in notifications) { if (notifications[i].id == id) { j = i; } }
                if (j != -1) {
                    meshserver.send({ action: 'intersession', subaction: 'removeNotify', id: id }); // Remove the notification in other sessions of the same user.
                    if (notifications[j].notification) { notifications[j].notification.close(); delete notifications[j].notification; }
                    notifications.splice(j, 1);
                    e.parentNode.removeChild(e);
                    setNotificationCount(notifications.length);
                    if (notifications.length == 0) { QV('notifiyBox', false); }
                    if (notifications.length == 1) { QV('notifyRemoveAll', false); }
                    if ((notifications.length > 0) && (j == 0)) {
                        var n = notifications[0];
                        QS('notifyx' + n.id)['border-top'] = '1px solid transparent';
                    }
                }
            }
        }

        // Add a new notification and play the notification sound
        function addNotification(n) {
            // Perform message translation
            var translatedTitles = [
                null,
                "Nueva Cuenta", // 1
                "Límite del Servidor",
                "Advertencia de Seguridad",
                "Configuraciones de la cuenta",
                "Grupo de Dispositivos",
                "Códigos de Invitación"
            ];
            var translatedMessages = [
                null,
                "Permiso denegado", // 1
                "Nombre de usuario no válido",
                "Contraseña no válida",
                "Correo electrónico no válido",
                "Dominio no válido",
                "Permisos de sitio no válidos",
                "El usuario ya existe",
                "No se puede agregar un usuario en este modo",
                "Excepción de validación",
                "Límite de cuenta alcanzado.", // 10
                "Solicitud de Chat, haga clic aquí para aceptar.",
                "Ha habido {0} intentos fallidos de inicio de sesión en esta cuenta desde el último inicio de sesión.",
                "No se pudo cambiar la dirección de correo electrónico, otra cuenta ya usa: {0}.",
                "Correo electrónico enviado.",
                "Usuario {0} no encontrado.",
                "Usuarios {0} no encontrados.",
                "Error, no se puede cambiar a la contraseña utilizada anteriormente.",
                "Error, no se puede cambiar a la contraseña de uso común.",
                "Error, contraseña no cambiada.",
                "Contraseña cambiada.", // 20
                "La contraseña actual no es correcta.",
                "Error, el código de invitación \"{0}\" ya está en uso.",
                "Puerta de enlace SMS no habilitada",
                "Sin derechos de gestión de usuarios",
                "Mensaje SMS no válido",
                "No hay número de teléfono para este usuario",
                "SMS enviado exitosamente.",
                "Error de SMS",
                "Error de SMS: {0}",
                "El dominio de correo electrónico \"{0}\" no está permitido. Solo ({1}) están permitidos", // 30,
                "Mensaje invalido",
                "Mensaje enviado con éxito.",
                "Mensaje de error",
                "Mensaje de error: {0}"
            ];
            if (typeof n.titleid == 'number') { try { n.title = translatedTitles[n.titleid]; } catch (ex) { } }
            if (typeof n.msgid == 'number') { try { n.text = translatedMessages[n.msgid]; if (Array.isArray(n.args)) { n.text = format(n.text, n.args[0], n.args[1], n.args[2], n.args[3], n.args[4], n.args[5]); } } catch (ex) { } }

            // Show notification within the web page.
            if (n.time == null) { n.time = Date.now(); }
            if (n.id == null) { n.id = Math.random(); }
            notifications.unshift(n);
            setNotificationCount(notifications.length);
            clickNotificationIcon(true);
            var notifySettings = getstore('notifications', 0);
            if (notifySettings & 1) { Q('chimes').play(); }

            // If web notifications are granted, use it.
            var notification = null;
            if (Notification && (Notification.permission == 'granted')) {
                var text = n.text.split('&reg;').join('').split('<b>').join('').split('</b>').join('').split('<br />').join('\r\n'); // Clean up any HTML codes
                if (n.nodeid) {
                    var node = getNodeFromId(n.nodeid);
                    if (node) {
                        if (notifySettings & 16) { // Notify with group name
                            notification = new Notification(decodeURIComponent('{{{extitle}}}') + ' - ' + meshes[node.meshid].name + ' - ' + node.name, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + node.icon + '.png' });
                        } else {
                            notification = new Notification(decodeURIComponent('{{{extitle}}}') + ' - ' + node.name, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + node.icon + '.png' });
                        }
                    }
                } else {
                    if (n.icon == null) { n.icon = 0; }
                    var title = n.title;
                    if (title == null) { title = ''; } else { title = ' - ' + n.title; }
                    notification = new Notification(decodeURIComponent('{{{extitle}}}') + title, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + n.icon + '.png' });
                }
                notification.id = n.id;
                notification.xtag = n.tag;
                notification.url = n.url;
                notification.nodeid = n.nodeid;
                notification.username = n.username;
                notification.onclick = function (e) { notificationSelected(e.target.id, true); }
                n.notification = notification;
            }

            // If the notification has a max time, setup the timer here.
            if ((typeof n.maxtime == 'number') && (n.maxtime > 0)) { var trigger = function notifyRemoveTrigger() { notificationDelete(notifyRemoveTrigger.xid); }; trigger.xid = n.id; setTimeout(trigger, n.maxtime * 1000); }
        }

        // Remove all notifications
        function deleteAllNotifications() {
            notifications = [];
            setNotificationCount(0);
            drawNotifications();
            QV('notifiyBox', false);
        }

        //
        // MyServer General
        //

        function setupGeneralServerStats() {
            // Destroy existing charts if they exist
            if (window.serverStatCpu) {
                window.serverStatCpu.destroy();
            }
            if (window.serverStatMemory) {
                window.serverStatMemory.destroy();
            }

            // Create new charts
            window.serverStatCpu = new Chart(document.getElementById('serverCpuChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#AAAAAA', '#00AA00'] }], labels: ["Usado", "Libre"] },
                options: { events: [], responsive: true, plugins: { legend: { display: false, } }, animation: { animateScale: true, animateRotate: true }, width: '40px' }
            });
            window.serverStatMemory = new Chart(document.getElementById('serverMemoryChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#AAAAAA', '#00AA00'] }], labels: ["Usado", "Libre"] },
                options: { events: [], responsive: true, plugins: { legend: { display: false, } }, animation: { animateScale: true, animateRotate: true }, width: '40px' }
            });
        }

        var lastServerStats = null;
        function updateGeneralServerStats(message) {
            if (message != null) { lastServerStats = message; } else { message = lastServerStats; }
            if (message == null) return;

            var serverStatsStrings = {
                ServerState: "Estado del Servidor",
                AgentErrorCounters: "Contador de Errores del Agente",
                UnknownGroup: "Grupo Desconocido",
                InvalidPKCSsignature: "Firma PKCS no válida",
                InvalidRSAsignature: "Firma RSA inválida",
                InvalidJSON: "JSON no válido.",
                UnknownAction: "Acción Desconocida",
                BadWebCertificate: "Certificado Web Erróneo",
                BadSignature: "Firma Errónea",
                MaxSessionsReached: "Sesiones Máximas Alcanzadas",
                UnknownDeviceGroup: "Grupo de Dispositivos Desconocido",
                InvalidDeviceGroupType: "Tipo de Grupo de Dispositivos No Válido",
                DuplicateAgent: "Duplicar Agente",
                ConnectedIntelAMT: "Conectado Intel&reg; AMT",
                ConnectedIntelAMTCira: "Conectado Intel&reg; AMT CIRA",
                RelayErrors: "Errores de Relé",
                UserAccounts: "Cuentas de Usuario",
                DeviceGroups: "Grupos de Dispositivos",
                AgentSessions: "Sesiones del Agente",
                ConnectedUsers: "Usuarios Conectados",
                UsersSessions: "Sesiones de Usuarios",
                RelaySessions: "Sesiones de Relé",
                RelayCount: "Contador de Relé"
            };

            setupGeneralServerStats();

            // Paint the pie graphs
            if (typeof message.cpuavg == 'object') {
                var m = Math.min(message.cpuavg[0], 1);
                window.serverStatCpu.config.data.datasets[0].data = [m, 1 - m];
                QH('serverCpuChartText', '<div style=margin-bottom:5px>' + "Carga del CPU" + '</div><div><b title="' + "Carga del CPU en el último minuto" + '">' + (Math.round(message.cpuavg[0] * 100.0) / 100.0) + '</b>, <b title="' + "Carga del CPU en los últimos 5 minutos" + '">' + (Math.round(message.cpuavg[1] * 100.0) / 100.0) + '</b>, <b title="' + "Carga del CPU en los últimos 15 minutos" + '">' + (Math.round(message.cpuavg[2] * 100.0) / 100.0) + '</b></div>');
                QS('serverCpuChartView')['display'] = 'inline-block';
                window.serverStatCpu.update();
            }
            if ((typeof message.totalmem == 'number') && (typeof message.availablemem == 'number')) {
                window.serverStatMemory.config.data.datasets[0].data = [message.totalmem - message.availablemem, message.availablemem];
                QH('serverMemoryChartText', '<div style=margin-bottom:5px>' + "Memoria Disponible" + '</div><div><b>' + getNiceSize3(message.availablemem) + '</b> ' + "libre" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "total" + '</div>');
                QS('serverMemoryChartView')['display'] = 'inline-block';
                window.serverStatMemory.update();
            } else if ((typeof message.totalmem == 'number') && (typeof message.freemem == 'number')) {
                window.serverStatMemory.config.data.datasets[0].data = [message.totalmem - message.freemem, message.freemem];
                QH('serverMemoryChartText', '<div style=margin-bottom:5px>' + "Memoria" + '</div><div><b>' + getNiceSize3(message.freemem) + '</b> ' + "libre" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "total" + '</div>');
                QS('serverMemoryChartView')['display'] = 'inline-block';
                window.serverStatMemory.update();
            }

            // Display all of the server values
            var x = '<div style=width:100% cellpadding=0 cellspacing=0>';
            if (typeof message.values == 'object') {
                for (var i in message.values) {
                    x += '<div class=userTableHeader style=margin-bottom:4px;width:200px>' + (serverStatsStrings[i] ? serverStatsStrings[i] : i) + '</div>';
                    for (var j in message.values[i]) {
                        x += '<div style=display:inline-block class="me-2"><table class=serverStateTableCell><tr><td></td><td><span>' + (serverStatsStrings[j] ? serverStatsStrings[j] : j) + '</span><span style=float:right>' + message.values[i][j] + '</span></td><td></td></tr></table></div>';
                    }
                }
            }
            x += '</div>';

            QH('serverStatsTable', x);
        }

        //
        // MyServer Stats
        //

        var serverTimelineStats = null;
        var serverTimelineConfig = {
            type: 'line',
            data: { labels: [], datasets: [{ label: '', backgroundColor: 'rgba(255, 99, 132, .5)', borderColor: 'rgb(255, 99, 132)', data: [], fill: true }] },
            options: {
                animation: false,
                responsive: true,
                maintainAspectRatio: false,
                elements: { line: { cubicInterpolationMode: 'monotone' } },
                scales: {
                    x: { type: 'time', time: { tooltipFormat: 'll HH:mm' }, display: true, title: { display: false, text: '' } },
                    y: { type: 'linear', display: true, title: { display: true, text: '' }, stacked: true }
                }
            }
        };

        function refreshServerTimelineStats(stats) { if ((meshserver != null) && (meshserver.State == 2)) { meshserver.send({ action: 'servertimelinestats', hours: 24 * 30 }); } }
        function pastDate(hours) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); return t; }
        function setServerTimelineStats(stats) { serverTimelineStats = stats; updateServerTimelineStats(); }
        function addServerTimelineStats(stats) {
            if (serverTimelineStats == null) return;

            // Check if this new data is for our selected server
            var selectedServer = null;
            if (Q('p40server').value != null) {
                selectedServer = decodeURIComponent(Q('p40server').value);
                if (selectedServer == "") { selectedServer = null; }
            }
            if (stats.s != selectedServer) return;

            serverTimelineStats.push(stats);
            var chartType = Q('p40type').value;
            if (chartType == 0) { // Connections
                serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.conn.ca });
                serverTimelineConfig.data.datasets[1].data.push({ x: stats.time, y: stats.conn.cu });
                serverTimelineConfig.data.datasets[2].data.push({ x: stats.time, y: stats.conn.us });
                serverTimelineConfig.data.datasets[3].data.push({ x: stats.time, y: stats.conn.rs });
                serverTimelineConfig.data.datasets[4].data.push({ x: stats.time, y: stats.conn.am });
                if (stats.conn.amc != null) { serverTimelineConfig.data.datasets[5].data.push({ x: stats.time, y: stats.conn.amc }); }
            } else if (chartType == 1) { // Memory
                serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.mem.external / (1024 * 1024) });
                serverTimelineConfig.data.datasets[1].data.push({ x: stats.time, y: stats.mem.heapUsed / (1024 * 1024) });
                serverTimelineConfig.data.datasets[2].data.push({ x: stats.time, y: stats.mem.heapTotal / (1024 * 1024) });
                serverTimelineConfig.data.datasets[3].data.push({ x: stats.time, y: stats.mem.rss / (1024 * 1024) });
            } else if ((chartType == 3) || (chartType == 4)) { // Traffic
                updateServerTimelineStats();
            } else if (chartType == 5) { // CPU
                if ((typeof stats.cpu == 'object') && (typeof stats.cpu[0] == 'number')) { serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.cpu[0] }); }
            }
            /* else if (chartType == 2) {
                serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.db.meshes });
                serverTimelineConfig.data.datasets[1].data.push({ x: stats.time, y: stats.db.nodes });
                serverTimelineConfig.data.datasets[2].data.push({ x: stats.time, y: stats.db.users });
                serverTimelineConfig.data.datasets[3].data.push({ x: stats.time, y: stats.db.total });
            } */
            updateServerTimelineHours();
        }
        function updateServerTimelineHours() {
            serverTimelineConfig.options.scales.y.type = (Q('p40log').checked ? 'logarithmic' : 'linear');
            serverTimelineConfig.options.scales.x.min = pastDate(Q('p40time').value);
            window.serverMainStats.update();
        }

        function setupServerTimelineStats() { window.serverMainStats = new Chart(document.getElementById('serverMainStats').getContext('2d'), serverTimelineConfig); }
        const trafficSeriesNames = ["Agente", "CIRA", "AMT-OS", "HTTP", "Relé", "Terminal", "Escritorio", "Archivos", "WebRDP", "WebSSH", "WebVNC", "Multiplex de Escritorio"];
        const seriesColors = [[158, 151, 16], [16, 84, 158], [255, 99, 132], [39, 158, 16], [134, 16, 158], [0, 148, 255], [255, 216, 0], [255, 127, 237], [109, 213, 255], [89, 94, 255], [179, 104, 255], [179, 104, 255]];
        function seriesColor1(n) { return 'rgba(' + (seriesColors[n][0] + ((255 - seriesColors[n][0]) / 1.5)) + ',' + (seriesColors[n][1] + ((255 - seriesColors[n][1]) / 1.5)) + ',' + (seriesColors[n][2] + ((255 - seriesColors[n][2]) / 1.5)) + ')'; }
        function seriesColor2(n) { return 'rgba(' + seriesColors[n][0] + ',' + seriesColors[n][1] + ',' + seriesColors[n][2] + ')'; }

        function updateServerTimelineStats() {
            if (serverTimelineStats == null) return;

            var data, chartType = Q('p40type').value, timeAfter = pastDate(Q('p40time').value), servers = [], selectedServer = null, serverEmptyExists = false, serverAutoSelect = true;
            if (Q('p40server').value != null) { selectedServer = decodeURIComponent(Q('p40server').value); if (selectedServer == "") { selectedServer = null; } serverAutoSelect = false; }
            serverTimelineConfig.options.scales.x.min = timeAfter;
            serverTimelineConfig.options.scales.y.stacked = ((chartType == 3) || (chartType == 4));
            if (chartType == 0) { // Connections
                serverTimelineConfig.options.scales.y.title.text = "Conteo de Conexión";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Agentes", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Usuarios", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Sesiones de Usuario", data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: "Sesiones de Relé", data: [], backgroundColor: 'rgba(39, 158, 16, .1)', borderColor: 'rgb(39, 158, 16)', fill: true },
                        { label: "Intel AMT", data: [], backgroundColor: 'rgba(134, 16, 158, .1)', borderColor: 'rgb(134, 16, 158)', fill: true },
                        { label: "Intel AMT CIRA", data: [], backgroundColor: 'rgba(255, 155, 0, .1)', borderColor: 'rgb(255, 155, 0)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    var t = new Date(serverTimelineStats[i].time);
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].first == true) {
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[1].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[2].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[3].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[4].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[5].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                    }
                    if (serverTimelineStats[i].conn) {
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.ca });
                        data.datasets[1].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.cu });
                        data.datasets[2].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.us });
                        data.datasets[3].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.rs });
                        data.datasets[4].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.am });
                        if (serverTimelineStats[i].conn.amc != null) { data.datasets[5].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.amc }); }
                    }
                }
            } else if (chartType == 1) { // Memory
                serverTimelineConfig.options.scales.y.title.text = "Megabytes";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Externo", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Heap Usado", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Heap Total", data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: "RSS", data: [], backgroundColor: 'rgba(39, 158, 16, .1)', borderColor: 'rgb(39, 158, 16)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].first == true) {
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[1].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[2].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[3].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                    }
                    data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.external / (1024 * 1024) });
                    data.datasets[1].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.heapUsed / (1024 * 1024) });
                    data.datasets[2].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.heapTotal / (1024 * 1024) });
                    data.datasets[3].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.rss / (1024 * 1024) });
                }
            } else if ((chartType == 3) || (chartType == 4)) { // Inbound/Outbound traffic
                serverTimelineConfig.options.scales.y.title.text = "Megabytes";
                data = { labels: [pastDate(0), timeAfter], datasets: [] };
                var seriesWithData = 0;

                // See that series have non-zero values
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if (serverTimelineStats[i].traffic == null) continue;
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].traffic) {
                        if (serverTimelineStats[i].traffic.AgentCtrlIn > 0) { seriesWithData |= 0x0001; } // Agent control traffic
                        if (serverTimelineStats[i].traffic.CIRAIn > 0) { seriesWithData |= 0x0002; } // Intel AMT CIRA traffic
                        if (serverTimelineStats[i].traffic.LMSIn > 0) { seriesWithData |= 0x0004; } // Intel AMT LMS traffic
                        if (serverTimelineStats[i].traffic.httpIn > 0) { seriesWithData |= 0x0008; } // HTTP traffic
                        if (serverTimelineStats[i].traffic.relayIn) {
                            if (serverTimelineStats[i].traffic.relayIn[0] > 0) { seriesWithData |= 0x0010; } // Relay traffic = 0
                            if (serverTimelineStats[i].traffic.relayIn[1] > 0) { seriesWithData |= 0x0020; } // Terminal traffic = 1
                            if (serverTimelineStats[i].traffic.relayIn[2] > 0) { seriesWithData |= 0x0040; } // Desktop traffic = 2
                            if (serverTimelineStats[i].traffic.relayIn[5] > 0) { seriesWithData |= 0x0080; } // Files traffic = 5
                            if (serverTimelineStats[i].traffic.relayIn[10] > 0) { seriesWithData |= 0x0100; } // WebRDP traffic = 10
                            if (serverTimelineStats[i].traffic.relayIn[11] > 0) { seriesWithData |= 0x0200; } // WebSSH traffic = 11
                            if (serverTimelineStats[i].traffic.relayIn[12] > 0) { seriesWithData |= 0x0400; } // WebVNC traffic = 12
                        }
                        if (serverTimelineStats[i].traffic.desktopMultiplex) {
                            if (serverTimelineStats[i].traffic.desktopMultiplex.in > 0) { seriesWithData |= 0x0800; } // Desktop Multiplex traffic
                        }
                    }
                }

                // Setup data series labels
                for (var i = 0; i < 13; i++) { if (seriesWithData & (1 << i)) { data.datasets.push({ label: trafficSeriesNames[i], data: [], backgroundColor: seriesColor1(i), borderColor: seriesColor2(i), fill: true, steppedLine: true }); } }

                // Place all of the series data
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if (serverTimelineStats[i].traffic == null) continue;
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].first == true) { for (var j = 1; j < 0xF000; j = j << 1) { if (seriesWithData & j) { data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN }); } } }
                    var z = 0;
                    if (chartType == 3) {
                        if (seriesWithData & 0x0001) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.AgentCtrlIn ? (serverTimelineStats[i].traffic.AgentCtrlIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0002) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.CIRAIn ? (serverTimelineStats[i].traffic.CIRAIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0004) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.LMSIn ? (serverTimelineStats[i].traffic.LMSIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0008) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.httpIn ? (serverTimelineStats[i].traffic.httpIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0010) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[0]) ? (serverTimelineStats[i].traffic.relayIn[0] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0020) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[1]) ? (serverTimelineStats[i].traffic.relayIn[1] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0040) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[2]) ? (serverTimelineStats[i].traffic.relayIn[2] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0080) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[5]) ? (serverTimelineStats[i].traffic.relayIn[5] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0100) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[10]) ? (serverTimelineStats[i].traffic.relayIn[10] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0200) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[11]) ? (serverTimelineStats[i].traffic.relayIn[11] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0400) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[12]) ? (serverTimelineStats[i].traffic.relayIn[12] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0800) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.desktopMultiplex && serverTimelineStats[i].traffic.desktopMultiplex.in) ? (serverTimelineStats[i].traffic.desktopMultiplex['in'] / 0x100000) : 0 }); } // We have to put ['in'] here because the language translator will fail if using .in
                    } else if (chartType == 4) {
                        if (seriesWithData & 0x0001) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.AgentCtrlOut ? (serverTimelineStats[i].traffic.AgentCtrlOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0002) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.CIRAOut ? (serverTimelineStats[i].traffic.CIRAOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0004) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.LMSOut ? (serverTimelineStats[i].traffic.LMSOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0008) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.httpOut ? (serverTimelineStats[i].traffic.httpOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0010) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[0]) ? (serverTimelineStats[i].traffic.relayOut[0] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0020) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[1]) ? (serverTimelineStats[i].traffic.relayOut[1] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0040) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[2]) ? (serverTimelineStats[i].traffic.relayOut[2] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0080) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[5]) ? (serverTimelineStats[i].traffic.relayOut[5] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0100) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[10]) ? (serverTimelineStats[i].traffic.relayOut[10] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0200) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[11]) ? (serverTimelineStats[i].traffic.relayOut[11] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0400) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[12]) ? (serverTimelineStats[i].traffic.relayOut[12] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0800) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.desktopMultiplex && serverTimelineStats[i].traffic.desktopMultiplex.out) ? (serverTimelineStats[i].traffic.desktopMultiplex.out / 0x100000) : 0 }); }
                    }
                }
            } else if (chartType == 5) { // CPU
                serverTimelineConfig.options.scales.y.title.text = "Uso";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "CPU", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if ((typeof serverTimelineStats[i].cpu == 'object') && (typeof serverTimelineStats[i].cpu[0] == 'number')) {
                        if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                            servers.push(serverTimelineStats[i].s);
                            if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                        }
                        if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                        if (serverTimelineStats[i].s != selectedServer) { continue; }
                        if (serverTimelineStats[i].first == true) {
                            data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        }
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].cpu[0] });
                    }
                }
            }
            /*else if (chartType == 2) { // Database
                serverTimelineConfig.options.scales.y.title.text = 'Records';
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: 'Groups', data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: 'Devices', data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: 'Users', data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: 'Records', data: [], backgroundColor: 'rgba(39, 158, 16, .1)', borderColor: 'rgb(39, 158, 16)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.meshes });
                    data.datasets[1].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.nodes });
                    data.datasets[2].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.users });
                    data.datasets[3].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.total });
                }
            }*/
            serverTimelineConfig.data = data;
            window.serverMainStats.update();

            if (servers.length > 0) {
                var x = '';
                for (var i = 0; i < servers.length; i++) { x += '<option value="' + encodeURIComponentEx(servers[i]) + '"' + ((selectedServer == servers[i]) ? ' selected' : '') + '>' + EscapeHtml(servers[i]) + '</option>'; }
                if (serverEmptyExists) { x += '<option value=""' + ((selectedServer == null) ? ' selected' : '') + '>' + "Nulo" + '</option>'; }
                QH('p40server', x);
            }
            QV('p40server', servers.length > 0);
        }

        function p40downloadEvents() {
            var csv = "time, conn.agent, conn.users, conn.usersessions, conn.relaysession, conn.intelamt, mem.external, mem.heapused, mem.heaptotal, mem.rss" + '\r\n';
            for (var i = 0; i < serverTimelineStats.length; i++) {
                if (serverTimelineStats[i].conn && serverTimelineStats[i].mem) {
                    csv += new Date(serverTimelineStats[i].time) + ', ' + serverTimelineStats[i].conn.ca + ', ' + serverTimelineStats[i].conn.cu + ', ' + serverTimelineStats[i].conn.us + ', ' + serverTimelineStats[i].conn.rs + ', ' + (serverTimelineStats[i].conn.am ? serverTimelineStats[i].conn.am : '') + ', ' + serverTimelineStats[i].mem.external + ', ' + serverTimelineStats[i].mem.heapUsed + ', ' + serverTimelineStats[i].mem.heapTotal + ', ' + serverTimelineStats[i].mem.rss + '\r\n';
                }
            }
            saveAs(stringToUtf8Blob(csv), 'ServerStats.csv');
        }

        //
        // My Server Tracing
        //

        var serverTrace = [];
        var serverTraceSources = [];

        function displayServerTrace() {
            var x = '', max = parseInt(Q('p41limitdropdown').value);
            if (serverTrace.length > max) { serverTrace.splice(max); }
            for (var i in serverTrace) {
                var args = [];
                for (var j in serverTrace[i].args) { if (typeof serverTrace[i].args[j] == 'object') { args.push(JSON.stringify(serverTrace[i].args[j])); } else { args.push(serverTrace[i].args[j]); } }
                x += '<div class=traceEvent onclick=showTraceEvent(' + i + ')>' + EscapeHtml(new Date(serverTrace[i].time).toLocaleTimeString()) + ' - <b>' + EscapeHtml(serverTrace[i].source.toUpperCase()) + '</b>: ' + EscapeHtml(args.join(', ')) + '</div>';
            }
            QH('p41events', x);
        }

        function showTraceEvent(i) {
            var e = serverTrace[i], x = '';
            if (xxdialogMode || (e == null)) return;
            for (var i in e.args) {
                if (typeof e.args[i] == 'object') { x += EscapeHtml(JSON.stringify(e.args[i])) + '<br /><br />'; } else { x += EscapeHtml(e.args[i]) + '<br /><br />'; }
            }
            xxdialogButtons = 1;
            setModalContent('xxAddAgent', "Evento de Seguimiento del Servidor", '<div class=selecttext style=max-height:300px;overflow-y:auto>' + x + '</div>');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', null);
        }

        function clearServerTracing() { serverTrace = []; displayServerTrace(); }

        function setServerTracing() {
            var x = '<div style="max-height:200px;overflow-y:auto">';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Servidor Central" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c1 ' + ((serverTraceSources.indexOf('cookie') >= 0) ? 'checked' : '') + '>' + "Codificador de Cookie" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c2 ' + ((serverTraceSources.indexOf('dispatch') >= 0) ? 'checked' : '') + '>' + "Despachador de Mensajes" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c3 ' + ((serverTraceSources.indexOf('main') >= 0) ? 'checked' : '') + '>' + "Mensajes del Servidor Principal" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c4 ' + ((serverTraceSources.indexOf('peer') >= 0) ? 'checked' : '') + '>' + "Emparejamiento de Servidores MeshCentral" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c15 ' + ((serverTraceSources.indexOf('agent') >= 0) ? 'checked' : '') + '>' + "Tráfico del MeshAgent" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c14 ' + ((serverTraceSources.indexOf('agentupdate') >= 0) ? 'checked' : '') + '>' + "Actualización del Agente Mesh" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c16 ' + ((serverTraceSources.indexOf('cert') >= 0) ? 'checked' : '') + '>' + "Certificado del Servidor" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c17 ' + ((serverTraceSources.indexOf('db') >= 0) ? 'checked' : '') + '>' + "Base de Datos del Servidor" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c18 ' + ((serverTraceSources.indexOf('email') >= 0) ? 'checked' : '') + '>' + "Correo electrónico / SMS / Tráfico push" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Servidor Web" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c5 ' + ((serverTraceSources.indexOf('web') >= 0) ? 'checked' : '') + '>' + "Servidor Web" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c6 ' + ((serverTraceSources.indexOf('webrequest') >= 0) ? 'checked' : '') + '>' + "Solicitudes al Servidor Web" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c7 ' + ((serverTraceSources.indexOf('relay') >= 0) ? 'checked' : '') + '>' + "Web Socket Relay" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c20 ' + ((serverTraceSources.indexOf('httpheaders') >= 0) ? 'checked' : '') + '>' + "Encabezados HTTP del Servidor Web" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c21 ' + ((serverTraceSources.indexOf('authlog') >= 0) ? 'checked' : '') + '>' + "Log de autenticacion de usuario" + '</label></div>';
            //x += '<div><label><input type=checkbox id=p41c8 ' + ((serverTraceSources.indexOf('webrelaydata') >= 0) ? 'checked' : '') + '>' + "Traffic Relay 2 Data" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Intel&reg; AMT" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c19 ' + ((serverTraceSources.indexOf('amt') >= 0) ? 'checked' : '') + '>' + "Gerente de Intel AMT" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c9 ' + ((serverTraceSources.indexOf('webrelay') >= 0) ? 'checked' : '') + '>' + "Relay de Conexión" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c10 ' + ((serverTraceSources.indexOf('mps') >= 0) ? 'checked' : '') + '>' + "Servidor CIRA" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c11 ' + ((serverTraceSources.indexOf('mpscmd') >= 0) ? 'checked' : '') + '>' + "Comandos del Servidor CIRA" + '</label></div>';
            //x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>Legacy</b></div>';
            //x += '<div><label><input type=checkbox id=p41c12 ' + ((serverTraceSources.indexOf('swarm') >= 0) ? 'checked' : '') + ">' + "Legacy Swarm Server" + '</label></div>";
            //x += '<div><label><input type=checkbox id=p41c13 ' + ((serverTraceSources.indexOf('swarmcmd') >= 0) ? 'checked' : '') + ">' + "Legacy Swarm Server Commands" + '</label></div>";
            x += '</div>';
            //setDialogMode(2, "Server Tracing", 7, setServerTracingEx, x);
            setModalContent('xxAddAgent', "Seguimiento del Servidor", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', setServerTracingEx);
        }

        function setServerTracingEx() {
            var b = 1;
            var sources = [], allsources = ['cookie', 'dispatch', 'main', 'peer', 'web', 'webrequest', 'relay', 'webrelaydata', 'webrelay', 'mps', 'mpscmd', 'swarm', 'swarmcmd', 'agentupdate', 'agent', 'cert', 'db', 'email', 'amt', 'httpheaders', 'authlog'];
            if (b == 1) { for (var i = 1; i < 22; i++) { try { if (Q('p41c' + i).checked) { sources.push(allsources[i - 1]); } } catch (ex) { } } }
            meshserver.send({ action: 'traceinfo', traceSources: sources });
        }

        function p41downloadServerTrace() {
            var csv = "time, source, message" + '\r\n';
            for (var i in serverTrace) { csv += '"' + new Date(serverTrace[i].time).toLocaleTimeString() + '","' + serverTrace[i].source + '","' + serverTrace[i].args.join(', ') + '"\r\n'; }
            saveAs(stringToUtf8Blob(csv), 'servertrace.csv');
            return false;
        }


        //
        // SERVICE WORKER
        //

        function setupServiceWorker() {
            if ((features2 & 8) == 0) return; // Web push not enabled on this server
            if ((typeof serverinfo.vapidpublickey != 'string') || (Notification == null) || (Notification.permission != 'granted')) return;
            // Register the service worker
            if ('serviceWorker' in navigator) {
                navigator.serviceWorker.register('serviceworker.js')
                    .then(function (reg) {
                        // Subscribe to push notifications
                        navigator.serviceWorker.ready.then(function (reg) {
                            reg.pushManager.subscribe({ applicationServerKey: urlBase64ToUint8Array(serverinfo.vapidpublickey), userVisibleOnly: true }).then(function (sub) {
                                meshserver.send({ action: 'webpush', sub: sub });
                            }).catch(function (e) { console.error('Worker: Unable to subscribe to push', e); });
                        })
                    }).catch(function (error) {
                        // Registration failed
                        console.log('Worker: Registration failed', error);
                    });
            }
        }

        //
        // POPUP DIALOG
        //

        // null = Hidden, 1 = Generic Message
        var xxdialogMode;
        var xxdialogFunc;
        var xxdialogButtons;
        var xxdialogTag;
        var xxcurrentView = -1;

        // Display a dialog box
        // Parameters: Dialog Mode (0 = none), Dialog Title, Buttons (1 = OK, 2 = Cancel, 3 = OK & Cancel), Call back function(0 = Cancel, 1 = OK), Dialog Content (Mode 2 only)
        function setDialogMode(x, y, b, f, c, tag) {
            setSessionActivity();
            QV('uiMenu', false);
            xxdialogMode = x;
            xxdialogFunc = f;
            xxdialogButtons = b;
            xxdialogTag = tag;

            // Reset dialog size
            //QS('dialog').width = QS('dialog').top = QS('dialog').left = QS('dialog').right = QS('dialog').bottom = null;

            //QE('idx_dlgOkButton', true);
            //QV('idx_dlgOkButton', b & 1);
            //QV('idx_dlgCancelButton', b & 2);
            //Q('idx_dlgCancelButton').value = ((b == 2) ? "Close" : "Cancel");
            QV('id_dialogclose', (b & 2) || (b & 8));
            QV('idx_dlgDeleteButton', b & 4);
            QV('idx_dlgButtonBar', b & 7);
            QV('idx_dlgMoreButtons', b & 16);
            if (y) QH('xxAddAgentTitle', y);
            for (var i = 1; i < 24; i++) { QV('dialog' + i, i == x); } // Edit this line when more dialogs are added
            QV('dialog', x);
            if (c) { if (x == 2) { QH('id_dialogOptions', c); } else { QH('id_dialogMessage', c); } }
            if (x == 0) { if (xxModal) xxModal.hide(); }
            MoreToggle(false);
        }

        function dialogclose(x) {
            setSessionActivity();
            var f = xxdialogFunc, b = xxdialogButtons, t = xxdialogTag;
            if (((b & 8) || x) && f) f(x, t);
        }

        function center() {
            setSessionActivity();
            if (xxcurrentView == 11) { deskAdjust(); }
            else if (xxcurrentView == 10) { mainUpdate(256); }
            else if (xxcurrentView == 1) { mainUpdate(4); }
        }
        function messagebox(t, m) { setSessionActivity(); QH('id_dialogMessage', m); setDialogMode(1, t, 1); }
        function statusbox(t, m) { setSessionActivity(); QH('id_dialogMessage', m); setDialogMode(1, t); }

        var backStack = [];
        function goBack() {
            setSessionActivity();
            if (xxdialogMode || (goBackStack.length == 0)) return;
            if (fullscreen) { deskToggleFull(); }
            go(goBackStack.pop());
            goBackStack.pop();
        }

        function goForward(id) { if (xxdialogMode) return; backStack.push(id); goStack(); }
        function goStack() {
            if (backStack.length == 0) { go(2); return; }
            var id = backStack[backStack.length - 1], idtype = id.split('/')[0];
            if (idtype == 'node') { setupDeviceMenu(0); gotoDevice(id); }
            if (idtype == 'mesh') { gotoMesh(id); }
            if (idtype == 'devices') { go(1); }
            if (idtype == 'account') { go(2); }
            if (idtype == 'events') { go(3); }
            if (idtype == 'users') { go(4); }
            if (idtype == 'files') {
                // Remind the user to add two factor authentication
                if ((features & 0x00040000) && !((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0) || (userinfo.otpkeys > 0) || ((features & 0x00800000) && (userinfo.otpekey == 1)))) { setDialogMode(2, "Seguridad de la Cuenta", 1, null, "Unable to access this feature until two-factor authentication is enabled. This is required for extra security. Go to the \"My Account\" and look at the \"Account Security\" section."); return; }
                go(5);
            }
            if (idtype == 'server') { go(6); }
        }

        function go(x, event) {
            if (event && ((event.which == 3) || (event.button == 2))) return; // ignore right clicks as they are handled elsewhere
            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0) && (x > 2)) {
                x = 2;
                setModalContent('xxAddAgent', "Seguridad de la Cuenta", 'Unable to access this feature until two - factor authentication is enabled.This is required for extra security.Go to the \"My Account\" tab and look at the \"Account Security\" section.');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }

            if (pluginHandler != null) pluginHandler.callHook('goPageStart', x, event);
            setSessionActivity();
            if (xxdialogMode) return;
            QV('uiMenu', false);

            // If "shift" is pressed, open a new tab.
            if (event && ((event.shiftKey == true) || (event.which == 2) || (event.button == 1)) && (x != 15) && ('{{{currentNode}}}'.toLowerCase() == '')) {
                // Open the device in a different tab
                event.preventDefault();
                if ((x >= 10) && (x <= 19)) {
                    if (currentNode) { safeNewWindow(window.location.origin + '{{{domainurl}}}' + '?gotonode=' + currentNode._id.split('/')[2] + '&viewmode=' + x + '&hide=16' + ((urlargs.key) ? ('&key=' + urlargs.key) : ''), 'meshcentral:' + currentNode._id); }
                } else if ((x >= 20) && (x <= 29)) {
                    if (currentMesh) { safeNewWindow(window.location.origin + '{{{domainurl}}}' + '?gotomesh=' + currentMesh._id.split('/')[2] + '&viewmode=' + x + '&hide=16' + ((urlargs.key) ? ('&key=' + urlargs.key) : ''), 'meshcentral:' + currentMesh._id); }
                } else if ((x >= 30) && (x <= 39)) {
                    if (currentUser) { safeNewWindow(window.location.origin + '{{{domainurl}}}' + '?gotouser=' + ((serverinfo.crossDomain) ? currentUser._id : currentUser._id.split('/')[2]) + '&viewmode=' + x + '&hide=16' + ((urlargs.key) ? ('&key=' + urlargs.key) : ''), 'meshcentral:' + currentUser._id); }
                } else if ((x >= 50) && (x <= 59)) {
                    if (currentUserGroup) { safeNewWindow(window.location.origin + '{{{domainurl}}}' + '?gotougrp=' + ((serverinfo.crossDomain) ? currentUserGroup._id : currentUserGroup._id.split('/')[2]) + '&viewmode=' + x + '&hide=16' + ((urlargs.key) ? ('&key=' + urlargs.key) : ''), 'meshcentral:' + currentUserGroup._id); }
                } else { // if (x < 10))
                    safeNewWindow(window.location.origin + '{{{domainurl}}}' + '?viewmode=' + x + '&hide=0' + ((urlargs.key) ? ('&key=' + urlargs.key) : ''), 'meshcentral:' + x);
                }
                return;
            }

            // If we are going to the same place, do nothing.
            if (xxcurrentView == x) return;

            // Set the goback stack, if going to top-level view, clear the stack.
            if ((xxcurrentView < 0) || (x < 10)) { goBackStack = []; } else {
                // Do not push into the back stack if we are changing tabs at the same level.
                if ((xxcurrentView == 50) || (Math.floor(xxcurrentView / 10) != Math.floor(x / 10))) { goBackStack.push(xxcurrentView); }
            }

            // If we are recording the desktop, stop it now.
            if ((xxcurrentView == 11) && (desktop != null) && (desktop.m.recordedData != null)) { deskRecordSession(); }

            // If we are trying to go to "My Users" and we are not a user manager, move to recordings
            if (((x == 4) && (userinfo != null) && ((userinfo.siteadmin & 2) == 0)) || ((features & 4) != 0) && (userinfo != null) && (userinfo._id.split('/')[2][0] != '~')) { x = 52; }

            // Stop the list graph if active
            if (xxcurrentView == 17) deviceDetailsStatsClear();

            // Edit this line when adding a new screen
            for (var i = 0; i < 61; i++) { QV('p' + i, i == x); }
            xxcurrentView = x;

            // Get out of fullscreen if needed
            if (fullscreen) { deskToggleFull(); }

            // Change the URL
            if (((features & 0x10000000) == 0) && (xxcurrentView > 0)) {
                var urlviewmode = '';
                if ((xxcurrentView >= 10) && (xxcurrentView <= 19)) { // Device Link
                    if (currentNode != null) { urlviewmode = '?viewmode=' + xxcurrentView + '&gotonode=' + currentNode._id.split('/')[2]; }
                } else if ((xxcurrentView >= 20) && (xxcurrentView <= 29)) { // Device Group Link
                    if (currentMesh != null) { urlviewmode = '?viewmode=' + xxcurrentView + '&gotomesh=' + currentMesh._id.split('/')[2]; }
                } else if ((xxcurrentView >= 30) && (xxcurrentView <= 39)) { // User Link
                    if (currentUser != null) { urlviewmode = '?viewmode=' + xxcurrentView + '&gotouser=' + ((serverinfo.crossDomain) ? currentUser._id : currentUser._id.split('/')[2]); }
                } else if ((xxcurrentView >= 51) && (xxcurrentView <= 51)) { // User Group Link
                    if ((currentUserGroup != null) && (currentUserGroup._id != null)) { urlviewmode = '?viewmode=' + xxcurrentView + '&gotougrp=' + ((serverinfo.crossDomain) ? currentUserGroup._id : currentUserGroup._id.split('/')[2]); }
                } else if (xxcurrentView > 1) { urlviewmode = '?viewmode=' + xxcurrentView; }
                for (var i in urlargs) { urlviewmode += (((urlviewmode == '') ? '?' : '&') + i + '=' + urlargs[i]); }
                try { window.history.replaceState({}, document.title, window.location.pathname + urlviewmode); } catch (ex) { }
            }

            // Remove top bar selection
            var mainBarItems = ['MainMenuMyDevices', 'MainMenuMyAccount', 'MainMenuMyEvents', 'MainMenuMyFiles', 'MainMenuMyUsers', 'MainMenuMyServer'];
            for (var i in mainBarItems) {
                QC(mainBarItems[i]).remove('fullselect');
                QC(mainBarItems[i]).remove('semiselect');
            }

            // Remove left bar selection
            var leftBarItems = ['LeftMenuMyDevices', 'LeftMenuMyAccount', 'LeftMenuMyEvents', 'LeftMenuMyFiles', 'LeftMenuMyUsers', 'LeftMenuMyServer'];
            for (var i in leftBarItems) {
                QC(leftBarItems[i]).remove('lbbuttonsel');
                QC(leftBarItems[i]).remove('lbbuttonsel2');
            }

            // Define class for Menu(s) as fully or semi active.
            var mainMenuActiveClass = (x < 9 ? 'fullselect' : 'semiselect');
            var leftMenuActiveClass = (((x < 9) || (x == 115) || (x == 40) || (x == 41) || (x == 42)) ? 'lbbuttonsel2' : 'lbbuttonsel');

            var backView = 0;
            if (goBackStack.length > 0) { backView = goBackStack[goBackStack.length - 1]; }

            // My Devices
            if (x == 1 || (backView == 1) || ((backView == 0) && (x >= 10 && x < 20))) {
                QC('MainMenuMyDevices').add(mainMenuActiveClass);
                QC('LeftMenuMyDevices').add(leftMenuActiveClass);
            } else if (x == 2 || (backView == 2) || ((backView == 0) && (x >= 20 && x < 30))) {
                // My Account
                QC('MainMenuMyAccount').add(mainMenuActiveClass);
                QC('LeftMenuMyAccount').add(leftMenuActiveClass);
            } else if ((x == 3) || (x == 60)) {
                // My Events
                QC('MainMenuMyEvents').add(mainMenuActiveClass);
                QC('LeftMenuMyEvents').add(leftMenuActiveClass);
            } else if (x == 4 || (x >= 30 && x < 40) || (x == 50) || (x == 51) || (x == 52)) {
                // My Users
                QC('MainMenuMyUsers').add(mainMenuActiveClass);
                QC('LeftMenuMyUsers').add(leftMenuActiveClass);
            } else if (x == 5) {
                // My Files
                QC('MainMenuMyFiles').add(mainMenuActiveClass);
                QC('LeftMenuMyFiles').add(leftMenuActiveClass);
            } else if ((x == 6) || (x == 115) || (x >= 40 && x < 50)) {
                // My Server
                QC('MainMenuMyServer').add(mainMenuActiveClass);
                QC('LeftMenuMyServer').add(leftMenuActiveClass);
            }
            QV('ServerPlugins', pluginHandler != null);

            // column_l max-height
            if (webPageStackMenu && (x >= 10)) { QC('column_l').add('room4submenu'); } else { QC('column_l').remove('room4submenu'); }

            // If we are going to panel 0 in "full screen mode", hide the left bar.
            QV('topbar', x != 0);
            if ((x == 0) && (webPageFullScreen)) { QC('body').add('arg_hide'); }

            QV('MainSubMenuSpan', (x >= 10) && (x < 20));
            QV('MeshSubMenuSpan', (x >= 20) && (x < 30));
            QV('UserSubMenuSpan', (x >= 30) && (x < 40));
            QV('ServerSubMenuSpan', x == 6 || x == 115 || x == 40 || x == 41 || x == 42 || x == 43);
            QV('UsersSubMenuSpan', x == 4 || x == 50 || x == 52);
            QV('EventsSubMenuSpan', (x == 3) || (x == 60));
            var panels = { 3: 'EventsLive', 4: 'UsersGeneral', 10: 'MainDev', 11: 'MainDevDesktop', 12: 'MainDevTerminal', 13: 'MainDevFiles', 14: 'MainDevAmt', 15: 'MainDevConsole', 16: 'MainDevEvents', 17: 'MainDevInfo', 18: 'MainDevApps', 19: 'MainDevPlugins', 20: 'MeshGeneral', 21: 'MeshSummary', 30: 'UserGeneral', 31: 'UserEvents', 6: 'ServerGeneral', 40: 'ServerStats', 41: 'ServerTrace', 42: 'ServerPlugins', 50: 'UsersGroups', 52: 'UsersRecordings', 60: 'EventsReport', 115: 'ServerConsole' };


            for (var i in panels) {
                QC(panels[i]).remove('style3x');
                QC(panels[i]).remove('style3sel');
                QC(panels[i]).add((x == i) ? 'style3sel' : 'style3x');
            }

            // If going to the remote desktop tab, adjust the tab.
            if (x == 11) { deskAdjust(); }

            // Panel 115 is weird, it's panel 15 for device console but used as a server console.
            if (x == 115) { QV('p15', true); }
            QV('p15uploadCore', x != 115);
            QV('p15BackButton', (x != 115) && ((args.hide & 32) == 0) && ('{{currentNode}}'.toLowerCase() == '')); // For device console, only show the back button if not hidden and not in single device view mode.
            if ((x == 15) || (x == 115)) { setupConsole(); }

            if (x == 1) mainUpdate(4);

            // Setup web notifications
            if ((x == 2) && Notification) { QV('accountEnableNotificationsSpan', Notification.permission != 'granted'); }

            // Fetch the server timeline stats if needed
            if ((x == 40) && (serverTimelineStats == null)) { refreshServerTimelineStats(); }

            // MyServer Plugins
            if (x == 42) { refreshPluginLatest(); }

            // Update Mesh Summary
            if (x == 21) { p21updateMesh(); }

            // Update Recordings
            if (x == 52) { if (p52recordings == null) { refreshRecodings(); } updateRecordings(); }

            // Update terminal size
            if ((x == 12) && (xtermfit != null)) { xtermfit.fit(); xterm.focus(); }

            // Update the web page title
            if ((currentNode) && (x >= 10) && (x < 20)) {
                document.title = currentNode.name + ((meshes[currentNode.meshid]) ? (' - ' + meshes[currentNode.meshid].name) : '') + ' - ' + decodeURIComponent('{{{extitle}}}');
            } else {
                document.title = decodeURIComponent('{{{extitle}}}');
            }
            if (pluginHandler != null) pluginHandler.callHook('goPageEnd', x, event);

            // Some panels must not have scroll bars
            QS('column_l')['overflow'] = ([11, 12].indexOf(x) >= 0) ? 'hidden' : null;

            // Software Panel
			if (x == 18) {
				if (currentNode == null) { go(1); return; }
				if (installedAppsData.desktop.length == 0) loadInstalledApps();
			}
        }

        //
        // Plugin Management
        //

        function updatePluginList(versInfo) {
            if (pluginHandler == null) return;
            if (Array.isArray(versInfo)) { versInfo.forEach(function (v) { updatePluginList(v); }); }
            QV('pluginNoneNotice', installedPluginList.length == 0);
            if (installedPluginList.length) {
                if (versInfo != null) {
                    if (installedPluginList['version_info'] == null) installedPluginList['version_info'] = [];
                    installedPluginList['version_info'][versInfo.id] = versInfo;
                }
                var tr = Q('p42tbl').querySelectorAll('.p42tblRow');
                if (tr.length) {
                    for (var i in Object.values(tr)) {
                        tr[i].parentNode.removeChild(tr[i]);
                    }
                }
                var statusMap = {
                    0: {
                        'text': 'Disabled',
                        'color': '858483'
                    },
                    1: {
                        'text': 'Installed',
                        'color': '00aa00'
                    }
                };
                var statusAvailability = {
                    0: {
                        'install': 'Install',
                        'delete': 'Delete'
                    },
                    1: {
                        'disable': 'Disable',
                        'upgrade': 'Upgrade',
                        'reload': 'Reload',
                        'permissions': 'Permissions',
                        //  'downgrade': 'Downgrade' // disabling until plugins have prior versions available for better testing
                    }
                };
                var vers_not_compat = ' [ <span onclick="return setDialogMode(2, \'Compatibility Issue\', 1, null, \'This plugin version is not compatible with your MeshCentral installation, please upgrade MeshCentral first.\');" title="' + "Versión incompatible, por favor primero actualiza tu instalación de MeshCentral" + '" style="cursor: pointer; color:red;"> ! </span> ]';
                var vers_not_compat = ' [ <span onclick="return setModalContent(\'xxAddAgent\', \'Compatibility Issue\', \'This plugin version is not compatible with your MeshCentral installation, please upgrade MeshCentral first.\');showModal(\'xxAddAgentModal\', \'idx_dlgOkButton\');" title="' + "Versión incompatible, por favor primero actualiza tu instalación de MeshCentral" + '" style="cursor: pointer; color:red;"> ! </span> ]';

                var tbl = Q('p42tbl');
                installedPluginList.forEach(function (p) {
                    var cant_action = [];
                    if (p.hasAdminPanel == true && p.status) {
                        p.nameHtml = '<a onclick="return goPlugin(\'' + p.shortName + '\', \'' + p.name.replace(/'/g, "\\'") + '\');">' + EscapeHtml(p.name) + '</a>';
                    } else {
                        p.nameHtml = EscapeHtml(p.name);
                    }
                    p.statusText = statusMap[p.status].text;
                    p.statusColor = statusMap[p.status].color;

                    if (p.versionHistoryUrl == null) { cant_action.push('downgrade'); }
                    if (!p.status) { p.version = ' - '; } // It isn't technically installed, so no version number
                    p.upgradeAvail = "Comprobando...";
                    if (installedPluginList['version_info'] != null && installedPluginList['version_info'][p._id] != null) {
                        var vin = installedPluginList['version_info'][p._id];
                        if (vin.hasUpdate) {
                            p.upgradeAvail = '<a title="' + "Ver Registro de Cambios" + '" rel="noreferrer noopener" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
                        } else {
                            cant_action.push('upgrade');
                            if (p.status) p.upgradeAvail = "Actualizado";
                            else p.upgradeAvail = '<a title="' + "Ver Registro de Cambios" + '" rel="noreferrer noopener" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
                        }
                        if (!vin.meshCentralCompat) {
                            p.upgradeAvail += vers_not_compat;
                            cant_action.push('install');
                            cant_action.push('upgrade');
                        }
                    }

                    p.actions = '<select onchange="return pluginAction(this,\'' + p._id + '\');"><option value=""> --</option>';
                    var entries = Object.entries(statusAvailability[p.status]);
                    for (var k in entries) {
                        if (cant_action.indexOf(entries[k][0]) === -1) {
                            p.actions += '<option value="' + entries[k][0] + '">' + entries[k][1] + '</option>';
                        }
                    }
                    p.actions += '</select>';

                    var tpl = '<td><img style=margin-top:3px src=images/plugin24.png></td><td class=gradTable1>&nbsp;</td><td class=gradTable2>' + p.nameHtml + '</td><td class=gradTable2>' + EscapeHtml(p.description) + '</td><td class=gradTable2 style=text-align:center><a href="' + EscapeHtml(p.homepage) + '" rel="noreferrer noopener" target="_blank">Home</a></td><td class=gradTable2 style=text-align:center>' + EscapeHtml(p.version) + '</td><td style=text-align:center class="pluginUpgradeAvailable gradTable2">' + p.upgradeAvail + '</td><td class=gradTable2 style="text-align:center;color:#' + p.statusColor + '">' + p.statusText + '</td><td class="pluginAction gradTable2" style=text-align:center>' + p.actions + '</td><td class=gradTable3>&nbsp;</td>';
                    var tr = tbl.insertRow(-1);
                    tr.innerHTML = tpl;
                    tr.classList.add('p42tblRow');
                    tr.setAttribute('data-id', p._id);
                    tr.setAttribute('id', 'pluginRow-' + p._id);
                });
            } else {
                var tr = Q('p42tbl').querySelectorAll('.p42tblRow');
                for (var i in Object.values(tr)) { tr[i].parentNode.removeChild(tr[i]); }
            }
            if (versInfo == null) refreshPluginLatest();
        }

        function refreshPluginLatest() {
            if (pluginHandler == null) return;
            meshserver.send({ action: 'pluginLatestCheck' });
        }

        function distributeCore() {
            if (pluginHandler == null) return;
            meshserver.send({ action: 'distributeCore', nodes: nodes }); // All nodes the user has access to
            QV('pluginRestartNotice', false);
        }

        function pluginActionEx() {
            if (pluginHandler == null) return;
            var act = Q('lastPluginAct').value, id = Q('lastPluginId').value, pVersUrl = Q('lastPluginVersion').value;

            switch (act) {
                case 'upgrade':
                case 'install':
                    meshserver.send({ 'action': 'installplugin', 'id': id, 'version_only': false });
                    break;
                case 'downgrade':
                    Q('lastPluginVersion').querySelectorAll('option').forEach(function (opt) {
                        if (opt.value == pVersUrl) pVers = opt.text;
                    });
                    meshserver.send({ 'action': 'installplugin', 'id': id, 'version_only': { 'name': pVers, 'url': pVersUrl } });
                    break;
                case 'delete':
                    meshserver.send({ 'action': 'removeplugin', 'id': id });
                    break;
                case 'disable':
                    meshserver.send({ 'action': 'disableplugin', 'id': id });
                    break;
                case 'reload':
                    var plugin = null;
                    for (var i in installedPluginList) { if (installedPluginList[i]._id == id) { plugin = installedPluginList[i]; } }
                    if (plugin) {
                        meshserver.send({ 'action': 'reloadplugin', 'plugin': plugin.shortName });
                    }
                    break;
            }
            QV('pluginRestartNotice', false);
        }

        function pluginAction(elem, id) {
            if (pluginHandler == null) return;
            
            // Special handling for permissions - opens matrix dialog
            if (elem.value == 'permissions') {
                var plugin = null;
                for (var i in installedPluginList) { if (installedPluginList[i]._id == id) { plugin = installedPluginList[i]; } }
                if (plugin) {
                    openPluginPermissionsDialog(plugin);
                }
                elem.value = '';
                return;
            }
            
            if (elem.value == 'downgrade') {
                meshserver.send({ 'action': 'getpluginversions', 'id': id });
            } else {
                var plugin = null;
                for (var i in installedPluginList) { if (installedPluginList[i]._id == id) { plugin = installedPluginList[i]; } }
                setModalContent('xxAddAgent', "Acción de Plugin", format("¿Estás seguro de que desea {0} el plugin: {1}?", elem.value, plugin.name) + '<input id="lastPluginAct" type="hidden" value="' + elem.value + '" /><input id="lastPluginId" type="hidden" value="' + id + '" /><input id="lastPluginVersion" type="hidden" value="" />');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => pluginActionEx());
            }
            elem.value = '';
        }

        function goPlugin(pname, title) {
            if (pluginHandler == null) return;
            if (pname == null) { Q('p43iframe').src = ''; } else { QH('p43title', title); Q('p43iframe').src = '/pluginadmin.ashx?pin=' + pname; go(43); }
        }

        // Plugin Permissions Dialog - Custom Modal Implementation
        var currentPluginPermissions = null;
        var permissionListData = null;
        var permissionState = {}; // JavaScript state for permissions
        
        function openPluginPermissionsDialog(plugin) {
            meshserver.send({ action: 'getpluginpermissions', plugin: plugin.shortName });
            meshserver.send({ action: 'getpluginpermissionlist' });
            
            currentPluginPermissions = { plugin: plugin.shortName, name: plugin.name };
            
            // Create custom modal
            var modalHtml = createPluginPermissionsModal(plugin);
            document.body.insertAdjacentHTML('beforeend', modalHtml);
            
            // Show the custom modal
            var modal = Q('pluginPermModal');
            modal.style.display = 'block';
            document.body.classList.add('modal-open');
        }
        
        function createPluginPermissionsModal(plugin) {
            var x = '<div id="pluginPermModal" class="modal" style="display:none; position:fixed; z-index:10000; left:0; top:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5);">';
            x += '<div class="modal-dialog modal-xl" style="margin:30px auto; max-width:900px;">';
            x += '<div class="modal-content" style="background:#fff; border-radius:8px;">';
            
            // Header
            x += '<div class="modal-header" style="padding:15px 20px; border-bottom:1px solid #dee2e6; display:flex; justify-content:space-between; align-items:center;">';
            x += '<h5 class="modal-title" style="margin:0; font-size:18px;">' + "Permisos del complemento" + ' - ' + EscapeHtml(plugin.name) + '</h5>';
            x += '<button type="button" class="btn-close" onclick="closePluginPermModal()" aria-label="Close"></button>';
            x += '</div>';
            
            // Body
            x += '<div class="modal-body" id="pluginPermBody" style="padding:20px; max-height:70vh; overflow-y:auto;">';
            x += '<div style="text-align:center; padding:40px;"><i class="fa-solid fa-spinner fa-spin"></i> ' + "Cargando permisos" + '...</div>';
            x += '</div>';
            
            // Footer
            x += '<div class="modal-footer" style="padding:15px 20px; border-top:1px solid #dee2e6; display:flex; justify-content:space-between; align-items:center;">';
            x += '<div style="font-size:12px; color:#666;">' + "Cascada de permisos:" + ' Node → Mesh → Global → Default' + '</div>';
            x += '<div>';
            x += '<button type="button" class="btn btn-secondary" style="margin-right:10px;" onclick="closePluginPermModal()">' + "Cancelar" + '</button>';
            x += '<button type="button" class="btn btn-primary" onclick="savePluginPermissionsEx()">' + "Guardar" + '</button>';
            x += '</div>';
            x += '</div>';
            
            x += '</div></div></div>';
            
            return x;
        }
        
        function closePluginPermModal() {
            var modal = Q('pluginPermModal');
            if (modal) {
                modal.remove();
                document.body.classList.remove('modal-open');
            }
        }
        
        function handlePluginPermissions(msg) {
            if (msg.action != 'pluginPermissions') return;
            currentPluginPermissions.data = msg.permissions;
            renderPermissionMatrixEx();
        }
        
        function handlePluginPermissionList(msg) {
            if (msg.action != 'pluginPermissionList') return;
            permissionListData = msg.list;
            renderPermissionMatrixEx();
        }
        
        function renderPermissionMatrixEx() {
            if (!currentPluginPermissions || !currentPluginPermissions.data || !permissionListData) return;
            
            var data = currentPluginPermissions.data;
            var definitions = data.definitions || {};
            var permissions = data.permissions || {};
            var defaults = data.defaults || {};
            
            // Check if plugin has no permissions defined
            if (Object.keys(definitions).length === 0) {
                var body = document.getElementById('pluginPermBody');
                if (body) {
                    body.innerHTML = '<div class="alert alert-info">' + "Este complemento no ha definido ningún permiso." + '</div>';
                }
                return;
            }
            
            // Initialize permissionState from loaded data
            permissionState = {};
            window.meshOverridesState = {};
            window.nodeOverridesState = {};
            for (var permKey in definitions) {
                var permData = permissions[permKey] || { 
                    allowed: { users: [], userGroups: [], meshes: [], nodes: [] }, 
                    denied: { users: [], userGroups: [], meshes: [], nodes: [] },
                    meshOverrides: {}, 
                    nodeOverrides: {} 
                };
                permissionState[permKey] = {
                    allowed: { users: [], userGroups: [], meshes: [], nodes: [] },
                    denied: { users: [], userGroups: [], meshes: [], nodes: [] }
                };
                if (permData.allowed) {
                    if (permData.allowed.users) permissionState[permKey].allowed.users = permData.allowed.users.slice();
                    if (permData.allowed.userGroups) permissionState[permKey].allowed.userGroups = permData.allowed.userGroups.slice();
                    if (permData.allowed.meshes) permissionState[permKey].allowed.meshes = permData.allowed.meshes.slice();
                    if (permData.allowed.nodes) permissionState[permKey].allowed.nodes = permData.allowed.nodes.slice();
                }
                if (permData.denied) {
                    if (permData.denied.users) permissionState[permKey].denied.users = permData.denied.users.slice();
                    if (permData.denied.userGroups) permissionState[permKey].denied.userGroups = permData.denied.userGroups.slice();
                    if (permData.denied.meshes) permissionState[permKey].denied.meshes = permData.denied.meshes.slice();
                    if (permData.denied.nodes) permissionState[permKey].denied.nodes = permData.denied.nodes.slice();
                }
                
                // Initialize meshOverridesState and nodeOverridesState from loaded data
                window.meshOverridesState[permKey] = permData.meshOverrides || {};
                window.nodeOverridesState[permKey] = permData.nodeOverrides || {};
            }
            
            var x = '';
            
            // Custom tab buttons (no Bootstrap JS required)
            x += '<div class="mb-3" style="border-bottom:1px solid #dee2e6;">';
            x += '<button class="btn btn-sm btn-outline-primary me-1 active" id="btnTabGlobal" onclick="switchPermTab(\'global\')">' + "Global" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary me-1" id="btnTabMeshes" onclick="switchPermTab(\'meshes\')">' + "Grupos" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary" id="btnTabNodes" onclick="switchPermTab(\'nodes\')">' + "Nodos" + '</button>';
            x += '</div>';
            
            // Global Panel
            x += '<div id="panel-global">';
            x += buildPermissionSection(definitions, permissions, defaults, 'global', null);
            x += '</div>';
            
            // Meshes Panel (hidden by default)
            x += '<div id="panel-meshes" style="display:none;">';
            x += buildMeshNodeSection(definitions, permissions, 'mesh');
            x += '</div>';
            
            // Nodes Panel (hidden by default)
            x += '<div id="panel-nodes" style="display:none;">';
            x += buildMeshNodeSection(definitions, permissions, 'node');
            x += '</div>';
            
            x += '<input type="hidden" id="permPluginShortName" value="' + currentPluginPermissions.plugin + '" />';
            
            Q('pluginPermBody').innerHTML = x;
        }
        
        function switchPermTab(tab) {
            // Hide all panels
            Q('panel-global').style.display = 'none';
            Q('panel-meshes').style.display = 'none';
            Q('panel-nodes').style.display = 'none';
            
            // Reset all buttons
            Q('btnTabGlobal').className = 'btn btn-sm btn-outline-secondary me-1';
            Q('btnTabMeshes').className = 'btn btn-sm btn-outline-secondary me-1';
            Q('btnTabNodes').className = 'btn btn-sm btn-outline-secondary';
            
            // Show selected panel and highlight button
            Q('panel-' + tab).style.display = 'block';
            Q('btnTab' + tab.charAt(0).toUpperCase() + tab.slice(1)).className = 'btn btn-sm btn-primary me-1';
        }
        
        function buildPermissionSection(definitions, permissions, defaults, level, parentId, overrideData) {
            var x = '<div id="permAccordion">';
            
            var idx = 0;
            for (var permKey in definitions) {
                var def = definitions[permKey];
                var permData = permissions[permKey] || { 
                    allowed: { users: [], userGroups: [], meshes: [], nodes: [] }, 
                    denied: { users: [], userGroups: [], meshes: [], nodes: [] },
                    meshOverrides: {}, 
                    nodeOverrides: {} 
                };
                var defaultVal = defaults[permKey] || 'inherited';
                
                var collapseId = 'permCollapse-' + level + '-' + permKey + (parentId ? '-' + parentId : '');
                var isFirst = idx === 0;
                
                // Determine which data to use: overrideData (for mesh/node) or permissionState (for global)
                var useData;
                if (overrideData && overrideData[permKey]) {
                    useData = overrideData[permKey];
                } else if (permissionState[permKey]) {
                    useData = permissionState[permKey];
                } else {
                    useData = { allowed: { users: [], userGroups: [], meshes: [], nodes: [] }, denied: { users: [], userGroups: [], meshes: [], nodes: [] } };
                }
                
                x += '<div class="mb-2" style="border:1px solid #dee2e6; border-radius:8px; overflow:hidden;">';
                x += '<div style="padding:12px 15px; background:#f8f9fa; display:flex; justify-content:space-between; align-items:center; cursor:pointer;" onclick="togglePermCollapse(\'' + collapseId + '\')">';
                x += '<div><strong>' + EscapeHtml(def.title || permKey) + '</strong><br><small class="text-muted">' + EscapeHtml(def.desc || '') + '</small></div>';
                x += '<div style="min-width:120px; text-align:right;"><select class="form-select form-select-sm" style="width:auto; display:inline-block;" id="perm_default_' + permKey + '" onclick="event.stopPropagation()">';
                x += '<option value="allowed"' + (defaultVal === 'allowed' ? ' selected' : '') + '>✓ ' + "Permitido" + '</option>';
                x += '<option value="denied"' + (defaultVal === 'denied' ? ' selected' : '') + '>✗ ' + "Denegado" + '</option>';
                x += '<option value="inherited"' + (defaultVal === 'inherited' ? ' selected' : '') + '>↩ ' + "Heredado" + '</option>';
                x += '</select></div>';
                x += '</div>';
                x += '<div id="' + collapseId + '" class="perm-collapse" style="padding:15px;' + (isFirst ? '' : 'display:none;') + '">';
                
                // Allowed section
                x += '<div class="row mb-3">';
                x += '<div class="col-md-6">';
                x += '<h6><span class="badge bg-success">' + "Permitido" + '</span></h6>';
                x += buildAutocompleteSection(permKey, level, 'allowed', parentId, useData ? useData.allowed : { users: [], userGroups: [], meshes: [], nodes: [] });
                x += '</div>';
                x += '<div class="col-md-6">';
                x += '<h6><span class="badge bg-danger">' + "Denegado" + '</span></h6>';
                x += buildAutocompleteSection(permKey, level, 'denied', parentId, useData ? useData.denied : { users: [], userGroups: [], meshes: [], nodes: [] });
                x += '</div>';
                x += '</div>';
                
                x += '</div></div>';
                idx++;
            }
            x += '</div>';
            
            return x;
        }
        
        function togglePermCollapse(id) {
            var el = Q(id);
            if (el.style.display === 'none') {
                el.style.display = 'block';
            } else {
                el.style.display = 'none';
            }
        }
        
        function buildAutocompleteSection(permKey, level, accessType, parentId, currentData) {
            var inputId = 'perm_input_' + level + '_' + accessType + '_' + permKey + (parentId ? '_' + parentId : '');
            var listId = 'perm_list_' + level + '_' + accessType + '_' + permKey + (parentId ? '_' + parentId : '');
            var containerId = 'perm_tags_' + level + '_' + accessType + '_' + permKey + (parentId ? '_' + parentId : '');
            
            var x = '<div class="position-relative">';
            x += '<input type="text" class="form-control form-control-sm" id="' + inputId + '" placeholder="' + "Buscar usuarios, grupos, dispositivos" + '..." ';
            x += 'onkeyup="searchPermEntities(this, \'' + permKey + '\', \'' + level + '\', \'' + accessType + '\', \'' + (parentId || '') + '\')" ';
            x += 'onfocus="showPermDropdown(\'' + listId + '\')" autocomplete="off">';
            x += '<div id="' + listId + '" class="list-group w-100" style="max-height:200px; overflow-y:auto; display:none; cursor:pointer;"></div>';
            x += '</div>';
            
            // Show selected tags
            x += '<div id="' + containerId + '" class="mt-2" style="display:flex; flex-wrap:wrap; gap:5px;">';
            
            // Render existing selections
            var allEntities = [];
            if (permissionListData.users) allEntities = allEntities.concat(permissionListData.users);
            if (permissionListData.userGroups) allEntities = allEntities.concat(permissionListData.userGroups);
            if (permissionListData.meshes) allEntities = allEntities.concat(permissionListData.meshes);
            if (permissionListData.nodes) allEntities = allEntities.concat(permissionListData.nodes);
            
            var selectedIds = currentData.users || [];
            selectedIds.forEach(function(id) {
                var entity = allEntities.find(function(e) { return e._id === id; });
                if (entity) {
                    x += buildPermTag(entity, permKey, level, accessType, 'user', parentId);
                }
            });
            
            var selectedGroups = currentData.userGroups || [];
            selectedGroups.forEach(function(id) {
                var entity = allEntities.find(function(e) { return e._id === id; });
                if (entity) {
                    x += buildPermTag(entity, permKey, level, accessType, 'userGroup', parentId);
                }
            });
            
            x += '</div>';
            
            return x;
        }
        
        function buildPermTag(entity, permKey, level, accessType, entityType, parentId) {
            var icon = entity._id.startsWith('user/') ? '👤' : (entity._id.startsWith('ugrp/') ? '👥' : (entity._id.startsWith('mesh/') ? '🖥️' : '💻'));
            var tagId = 'perm_tag_' + entity._id.replace(/[^a-zA-Z0-9]/g, '_') + '_' + permKey + '_' + level + '_' + accessType + (parentId ? '_' + parentId : '');
            var safeId = entity._id.replace(/'/g, '___');
            
            return '<span class="badge" style="background:#6c757d; padding:5px 8px; font-size:12px;" id="' + tagId + '" data-actualid="' + EscapeHtml(entity._id) + '" data-perm="' + permKey + '" data-level="' + level + '" data-access="' + accessType + '" data-type="' + entityType + '" data-parentid="' + (parentId || '') + '">' + icon + ' ' + EscapeHtml(entity.name || entity._id) + ' <i class="fa-solid fa-times" style="cursor:pointer; margin-left:5px;" onclick="removePermTagFromTag(this)"></i></span>';
        }
        
        function removePermTagFromTag(el) {
            var span = el.parentElement;
            removePermTag(span.getAttribute('data-actualid'), span.getAttribute('data-perm'), span.getAttribute('data-level'), span.getAttribute('data-access'), span.getAttribute('data-type'), span.getAttribute('data-parentid') || '');
        }
        
        function searchPermEntities(input, permKey, level, accessType, parentId) {
            var query = input.value.toLowerCase();
            var listId = 'perm_list_' + level + '_' + accessType + '_' + permKey + (parentId ? '_' + parentId : '');
            var list = Q(listId);
            if (!list) return;
            
            if (query.length < 1) {
                list.style.display = 'none';
                return;
            }
            
            // Get currently selected IDs to filter out
            var selectedIds = [];
            if (permissionState[permKey] && permissionState[permKey][accessType]) {
                var state = permissionState[permKey][accessType];
                if (state.users) selectedIds = selectedIds.concat(state.users);
                if (state.userGroups) selectedIds = selectedIds.concat(state.userGroups);
                if (state.meshes) selectedIds = selectedIds.concat(state.meshes);
                if (state.nodes) selectedIds = selectedIds.concat(state.nodes);
            }
            
            // Also check mesh/node overrides for the current context
            if (level === 'mesh' && parentId && window.meshOverridesState && window.meshOverridesState[permKey] && window.meshOverridesState[permKey][parentId] && window.meshOverridesState[permKey][parentId][accessType]) {
                var meshState = window.meshOverridesState[permKey][parentId][accessType];
                if (meshState.users) selectedIds = selectedIds.concat(meshState.users);
                if (meshState.userGroups) selectedIds = selectedIds.concat(meshState.userGroups);
                if (meshState.meshes) selectedIds = selectedIds.concat(meshState.meshes);
                if (meshState.nodes) selectedIds = selectedIds.concat(meshState.nodes);
            }
            if (level === 'node' && parentId && window.nodeOverridesState && window.nodeOverridesState[permKey] && window.nodeOverridesState[permKey][parentId] && window.nodeOverridesState[permKey][parentId][accessType]) {
                var nodeState = window.nodeOverridesState[permKey][parentId][accessType];
                if (nodeState.users) selectedIds = selectedIds.concat(nodeState.users);
                if (nodeState.userGroups) selectedIds = selectedIds.concat(nodeState.userGroups);
                if (nodeState.meshes) selectedIds = selectedIds.concat(nodeState.meshes);
                if (nodeState.nodes) selectedIds = selectedIds.concat(nodeState.nodes);
            }
            
            var results = [];
            var groupedNodes = {};
            
            // Search users
            if (permissionListData.users) {
                permissionListData.users.forEach(function(u) {
                    if (selectedIds.indexOf(u._id) >= 0) return;
                    if ((u.name && u.name.toLowerCase().includes(query)) || (u.email && u.email.toLowerCase().includes(query))) {
                        results.push({ _id: u._id, name: u.name || u.email, type: 'user', icon: '👤' });
                    }
                });
            }
            
            // Search user groups
            if (permissionListData.userGroups) {
                permissionListData.userGroups.forEach(function(ug) {
                    if (selectedIds.indexOf(ug._id) >= 0) return;
                    if (ug.name && ug.name.toLowerCase().includes(query)) {
                        results.push({ _id: ug._id, name: ug.name, type: 'userGroup', icon: '👥' });
                    }
                });
            }
            
            // Search meshes (only for global level)
            if (level === 'global' && permissionListData.meshes) {
                permissionListData.meshes.forEach(function(m) {
                    if (selectedIds.indexOf(m._id) >= 0) return;
                    if (m.name && m.name.toLowerCase().includes(query)) {
                        results.push({ _id: m._id, name: m.name, type: 'mesh', icon: '🖥️' });
                    }
                });
            }
            
            // Search nodes and group by mesh
            if (level === 'node' && permissionListData.nodes) {
                permissionListData.nodes.forEach(function(n) {
                    if (selectedIds.indexOf(n._id) >= 0) return;
                    if (n.name && n.name.toLowerCase().includes(query)) {
                        var meshGroup = n.meshname || 'Ungrouped';
                        if (!groupedNodes[meshGroup]) groupedNodes[meshGroup] = [];
                        groupedNodes[meshGroup].push({ _id: n._id, name: n.name, type: 'node', icon: '💻', meshname: meshGroup });
                    }
                });
            }
            
            list.innerHTML = '';
            
            // Add individual results first
            results.slice(0, 10).forEach(function(r) {
                var safeId = r._id.replace(/'/g, '___');
                list.innerHTML += '<a class="list-group-item list-group-item-action" style="padding:8px 12px; cursor:pointer;" data-id="' + safeId + '" data-actualid="' + EscapeHtml(r._id) + '" data-perm="' + permKey + '" data-level="' + level + '" data-access="' + accessType + '" data-type="' + r.type + '" data-name="' + EscapeHtml(r.name) + '" data-icon="' + r.icon + '" data-parentid="' + (parentId || '') + '" onclick="addPermEntityFromDropdown(this)">' + r.icon + ' ' + EscapeHtml(r.name) + ' <small class="text-muted">' + r.type + '</small></a>';
            });
            
            // Add grouped nodes at the end
            if (level === 'node') {
                for (var meshName in groupedNodes) {
                    list.innerHTML += '<div class="list-group-item" style="padding:8px 12px; background:#eee; font-weight:bold; font-size:11px;">🖥️ ' + EscapeHtml(meshName) + '</div>';
                    groupedNodes[meshName].forEach(function(n) {
                        var safeId = n._id.replace(/'/g, '___');
                        list.innerHTML += '<a class="list-group-item list-group-item-action" style="padding:8px 12px; cursor:pointer; padding-left:20px;" data-id="' + safeId + '" data-actualid="' + EscapeHtml(n._id) + '" data-perm="' + permKey + '" data-level="' + level + '" data-access="' + accessType + '" data-type="' + n.type + '" data-name="' + EscapeHtml(n.name) + '" data-icon="' + n.icon + '" data-parentid="' + (parentId || '') + '" onclick="addPermEntityFromDropdown(this)">' + n.icon + ' ' + EscapeHtml(n.name) + '</a>';
                    });
                }
            }
            
            list.style.display = (results.length > 0 || (level === 'node' && Object.keys(groupedNodes).length > 0)) ? 'block' : 'none';
        }
        
        function addPermEntityFromDropdown(el) {
            var entityId = el.getAttribute('data-actualid');
            var permKey = el.getAttribute('data-perm');
            var level = el.getAttribute('data-level');
            var accessType = el.getAttribute('data-access');
            var entityType = el.getAttribute('data-type');
            var entityName = el.getAttribute('data-name');
            var entityIcon = el.getAttribute('data-icon');
            var parentId = el.getAttribute('data-parentid') || '';
            addPermEntity(entityId, permKey, level, accessType, entityType, parentId, entityName, entityIcon);
        }
        
        function showPermDropdown(listId) {
            var list = Q(listId);
            if (list && list.children.length > 0) {
                list.style.display = 'block';
            }
        }
        
        function addPermEntity(entityId, permKey, level, accessType, entityType, parentId, entityName, entityIcon) {
            var containerId = 'perm_tags_' + level + '_' + accessType + '_' + permKey + (parentId ? '_' + parentId : '');
            var container = Q(containerId);
            var inputId = 'perm_input_' + level + '_' + accessType + '_' + permKey + (parentId ? '_' + parentId : '');
            var listId = 'perm_list_' + level + '_' + accessType + '_' + permKey + (parentId ? '_' + parentId : '');
            
            // Clear input and hide dropdown
            Q(inputId).value = '';
            Q(listId).style.display = 'none';
            
            // Check if already exists
            var existingTags = container.querySelectorAll('span');
            var tagId = 'perm_tag_' + entityId.replace(/[^a-zA-Z0-9]/g, '_') + '_' + permKey + '_' + level + '_' + accessType + (parentId ? '_' + parentId : '');
            for (var i = 0; i < existingTags.length; i++) {
                if (existingTags[i].id && existingTags[i].id == tagId) {
                    return; // Already added
                }
            }
            
            // Add new tag
            var tagHtml = '<span class="badge" style="background:#6c757d; padding:5px 8px; font-size:12px;" id="' + tagId + '" data-actualid="' + EscapeHtml(entityId) + '">' + entityIcon + ' ' + EscapeHtml(entityName) + ' <i class="fa-solid fa-times" style="cursor:pointer; margin-left:5px;" onclick="removePermTag(\'' + entityId + '\', \'' + permKey + '\', \'' + level + '\', \'' + accessType + '\', \'' + entityType + '\', \'' + (parentId || '') + '\')"></i></span>';
            container.insertAdjacentHTML('beforeend', tagHtml);
            
            // Initialize overrides storage
            if (!window.meshOverridesState) window.meshOverridesState = {};
            if (!window.nodeOverridesState) window.nodeOverridesState = {};
            if (!window.meshOverridesState[permKey]) window.meshOverridesState[permKey] = {};
            if (!window.nodeOverridesState[permKey]) window.nodeOverridesState[permKey] = {};
            
            var entityTypeKey = entityType === 'user' ? 'users' : (entityType === 'userGroup' ? 'userGroups' : (entityType === 'mesh' ? 'meshes' : (entityType === 'node' ? 'nodes' : null)));
            
            // Skip if entityTypeKey is invalid
            if (entityTypeKey === null) {
                console.log('Invalid entityType:', entityType);
                return;
            }
            
            // Update permissionState only for global level
            if (level === 'global') {
                if (permissionState[permKey] && permissionState[permKey][accessType] && permissionState[permKey][accessType][entityTypeKey]) {
                    if (permissionState[permKey][accessType][entityTypeKey].indexOf(entityId) === -1) {
                        permissionState[permKey][accessType][entityTypeKey].push(entityId);
                    }
                }
            }
            
            // Update mesh/node overrides if in context
            if (level === 'mesh' && parentId) {
                if (!window.meshOverridesState[permKey][parentId]) {
                    window.meshOverridesState[permKey][parentId] = { allowed: { users: [], userGroups: [], meshes: [], nodes: [] }, denied: { users: [], userGroups: [], meshes: [], nodes: [] } };
                }
                // Ensure accessType object exists
                if (!window.meshOverridesState[permKey][parentId][accessType]) {
                    window.meshOverridesState[permKey][parentId][accessType] = { users: [], userGroups: [], meshes: [], nodes: [] };
                }
                if (window.meshOverridesState[permKey][parentId][accessType][entityTypeKey] && window.meshOverridesState[permKey][parentId][accessType][entityTypeKey].indexOf(entityId) === -1) {
                    window.meshOverridesState[permKey][parentId][accessType][entityTypeKey].push(entityId);
                }
            }
            if (level === 'node' && parentId) {
                if (!window.nodeOverridesState[permKey][parentId]) {
                    window.nodeOverridesState[permKey][parentId] = { allowed: { users: [], userGroups: [], meshes: [], nodes: [] }, denied: { users: [], userGroups: [], meshes: [], nodes: [] } };
                }
                // Ensure accessType object exists
                if (!window.nodeOverridesState[permKey][parentId][accessType]) {
                    window.nodeOverridesState[permKey][parentId][accessType] = { users: [], userGroups: [], meshes: [], nodes: [] };
                }
                // Ensure entityTypeKey array exists
                if (!window.nodeOverridesState[permKey][parentId][accessType][entityTypeKey]) {
                    window.nodeOverridesState[permKey][parentId][accessType][entityTypeKey] = [];
                }
                if (window.nodeOverridesState[permKey][parentId][accessType][entityTypeKey] && window.nodeOverridesState[permKey][parentId][accessType][entityTypeKey].indexOf(entityId) === -1) {
                    window.nodeOverridesState[permKey][parentId][accessType][entityTypeKey].push(entityId);
                }
            }
        }
        
        function removePermTag(entityId, permKey, level, accessType, entityType, parentId) {
            var tagId = 'perm_tag_' + entityId.replace(/[^a-zA-Z0-9]/g, '_') + '_' + permKey + '_' + level + '_' + accessType + (parentId ? '_' + parentId : '');
            var tag = Q(tagId);
            if (tag) tag.remove();
            
            // Initialize overrides storage
            if (!window.meshOverridesState) window.meshOverridesState = {};
            if (!window.nodeOverridesState) window.nodeOverridesState = {};
            if (!window.meshOverridesState[permKey]) window.meshOverridesState[permKey] = {};
            if (!window.nodeOverridesState[permKey]) window.nodeOverridesState[permKey] = {};
            
            var entityTypeKey = entityType === 'user' ? 'users' : (entityType === 'userGroup' ? 'userGroups' : (entityType === 'mesh' ? 'meshes' : 'nodes'));
            
            // Remove from permissionState only for global level
            if (level === 'global') {
                if (permissionState[permKey] && permissionState[permKey][accessType] && permissionState[permKey][accessType][entityTypeKey]) {
                    var idx = permissionState[permKey][accessType][entityTypeKey].indexOf(entityId);
                    if (idx > -1) {
                        permissionState[permKey][accessType][entityTypeKey].splice(idx, 1);
                    }
                }
            }
            
            // Remove from mesh overrides if in mesh context
            if (level === 'mesh' && parentId && window.meshOverridesState[permKey][parentId]) {
                var override = window.meshOverridesState[permKey][parentId];
                if (override[accessType] && override[accessType][entityTypeKey]) {
                    var idx3 = override[accessType][entityTypeKey].indexOf(entityId);
                    if (idx3 > -1) {
                        override[accessType][entityTypeKey].splice(idx3, 1);
                    }
                }
            }
            
            // Remove from node overrides if in node context
            if (level === 'node' && parentId && window.nodeOverridesState[permKey][parentId]) {
                var nodeOverride = window.nodeOverridesState[permKey][parentId];
                if (nodeOverride[accessType] && nodeOverride[accessType][entityTypeKey]) {
                    var idx4 = nodeOverride[accessType][entityTypeKey].indexOf(entityId);
                    if (idx4 > -1) {
                        nodeOverride[accessType][entityTypeKey].splice(idx4, 1);
                    }
                }
            }
            
            // Update mesh overrides if in mesh context
            if (level === 'mesh' && parentId && window.meshOverridesState[permKey][parentId]) {
                var override = window.meshOverridesState[permKey][parentId];
                if (override[accessType] && override[accessType][entityTypeKey]) {
                    var idx3 = override[accessType][entityTypeKey].indexOf(entityId);
                    if (idx3 > -1) {
                        override[accessType][entityTypeKey].splice(idx3, 1);
                    }
                }
            }
            
            // Update node overrides if in node context
            if (level === 'node' && parentId && window.nodeOverridesState[permKey][parentId]) {
                var nodeOverride = window.nodeOverridesState[permKey][parentId];
                if (nodeOverride[accessType] && nodeOverride[accessType][entityTypeKey]) {
                    var idx4 = nodeOverride[accessType][entityTypeKey].indexOf(entityId);
                    if (idx4 > -1) {
                        nodeOverride[accessType][entityTypeKey].splice(idx4, 1);
                    }
                }
            }
        }
        
        function buildMeshNodeSection(definitions, permissions, type) {
            var x = '<p class="text-muted">Select a ' + type + ' to configure permissions:</p>';
            x += '<select class="form-select mb-3" id="perm_' + type + 'Select" onchange="renderMeshNodePerms(\'' + type + '\', this.value)">';
            x += '<option value="">-- ' + (type === 'mesh' ? "Seleccionar grupo de dispositivos" : "Seleccionar dispositivo" ) + ' --</option>';
            
            if (type === 'mesh') {
                var list = permissionListData.meshes;
                if (list) {
                    list.forEach(function(item) {
                        x += '<option value="' + EscapeHtml(item._id) + '">' + EscapeHtml(item.name) + '</option>';
                    });
                }
            } else {
                // Group nodes by mesh
                var groupedNodes = {};
                if (permissionListData.nodes) {
                    permissionListData.nodes.forEach(function(n) {
                        var meshGroup = n.meshname || 'Ungrouped';
                        if (!groupedNodes[meshGroup]) groupedNodes[meshGroup] = [];
                        groupedNodes[meshGroup].push(n);
                    });
                }
                for (var meshName in groupedNodes) {
                    x += '<optgroup label="🖥️ ' + EscapeHtml(meshName) + '">';
                    groupedNodes[meshName].forEach(function(item) {
                        x += '<option value="' + EscapeHtml(item._id) + '">  ' + EscapeHtml(item.name) + '</option>';
                    });
                    x += '</optgroup>';
                }
            }
            x += '</select>';
            
            x += '<div id="meshNodePermContent_' + type + '"></div>';
            
            return x;
        }
        
        function renderMeshNodePerms(type, selectedId) {
            var content = Q('meshNodePermContent_' + type);
            if (!selectedId) {
                content.innerHTML = '';
                return;
            }
            
            var data = currentPluginPermissions.data;
            var definitions = data.definitions || {};
            var permissions = data.permissions || {};
            var defaults = data.defaults || {};
            
            var overrideKey = type === 'mesh' ? 'meshOverrides' : 'nodeOverrides';
            
            // Extract override data for the selected mesh/node
            var overrideData = {};
            for (var pk in definitions) {
                if (permissions[pk] && permissions[pk][overrideKey] && permissions[pk][overrideKey][selectedId]) {
                    overrideData[pk] = permissions[pk][overrideKey][selectedId];
                } else {
                    overrideData[pk] = {
                        allowed: { users: [], userGroups: [], meshes: [], nodes: [] },
                        denied: { users: [], userGroups: [], meshes: [], nodes: [] }
                    };
                }
            }
            
            var displayName = '';
            var list = type === 'mesh' ? permissionListData.meshes : permissionListData.nodes;
            if (list) {
                var item = list.find(function(i) { return i._id === selectedId; });
                if (item) displayName = item.name;
            }
            
            // Store current override data for this mesh/node in a temporary location
            window.currentPermOverride = { type: type, id: selectedId, data: overrideData };
            
            var x = '<div class="alert alert-info"><strong>' + EscapeHtml(displayName) + '</strong> - ' + (type === 'mesh' ? "Permisos específicos para este grupo de dispositivos" : "Permisos específicos para este dispositivo") + '</div>';
            x += buildPermissionSection(definitions, permissions, defaults, type, selectedId, overrideData);
            
            content.innerHTML = x;
        }
        
        function updatePermBadge(permKey) {
            // Visual feedback when default changes
        }
        
        function savePluginPermissionsEx() {
            if (!currentPluginPermissions || !permissionListData) return;
            
            console.log('Saving permissionState:', JSON.stringify(permissionState));
            
            var pluginName = Q('permPluginShortName').value;
            var definitions = currentPluginPermissions.data.definitions || {};
            var permissions = {};
            var defaults = {};
            
            // Initialize mesh and node overrides storage if not exists
            if (!window.meshOverridesState) window.meshOverridesState = {};
            if (!window.nodeOverridesState) window.nodeOverridesState = {};
            
            // Collect defaults from DOM
            for (var permKey in definitions) {
                defaults[permKey] = Q('perm_default_' + permKey).value;
            }
            
            // Use permissionState for permissions (more reliable than DOM scraping)
            for (var permKey in definitions) {
                var state = permissionState[permKey] || { allowed: { users: [], userGroups: [], meshes: [], nodes: [] }, denied: { users: [], userGroups: [], meshes: [], nodes: [] } };
                permissions[permKey] = {
                    allowed: { users: [], userGroups: [], meshes: [], nodes: [] },
                    denied: { users: [], userGroups: [], meshes: [], nodes: [] },
                    meshOverrides: window.meshOverridesState[permKey] || {},
                    nodeOverrides: window.nodeOverridesState[permKey] || {}
                };
                
                // Copy from permissionState (global)
                if (state.allowed) {
                    permissions[permKey].allowed.users = state.allowed.users ? state.allowed.users.slice() : [];
                    permissions[permKey].allowed.userGroups = state.allowed.userGroups ? state.allowed.userGroups.slice() : [];
                    permissions[permKey].allowed.meshes = state.allowed.meshes ? state.allowed.meshes.slice() : [];
                    permissions[permKey].allowed.nodes = state.allowed.nodes ? state.allowed.nodes.slice() : [];
                }
                if (state.denied) {
                    permissions[permKey].denied.users = state.denied.users ? state.denied.users.slice() : [];
                    permissions[permKey].denied.userGroups = state.denied.userGroups ? state.denied.userGroups.slice() : [];
                    permissions[permKey].denied.meshes = state.denied.meshes ? state.denied.meshes.slice() : [];
                    permissions[permKey].denied.nodes = state.denied.nodes ? state.denied.nodes.slice() : [];
                }
            }
            
            console.log('Sending permissions:', JSON.stringify(permissions));
            
            meshserver.send({
                action: 'setpluginpermissions',
                plugin: pluginName,
                data: { permissions: permissions, defaults: defaults }
            });
            
            closePluginPermModal();
        }

        //
        // Access Control Functions
        // These must match server
        //

        // Remove user rights
        function removeUserRights(rights, userid) {
            if ((userid != userinfo._id) || (userinfo.removeRights == null)) return rights;
            var add = 0, substract = 0;
            if ((userinfo.removeRights & 0x00000008) != 0) { substract += 0x00000008; } // No Remote Control
            if ((userinfo.removeRights & 0x00010000) != 0) { add += 0x00010000; } // No Desktop
            if ((userinfo.removeRights & 0x00000100) != 0) { add += 0x00000100; } // Desktop View Only
            if ((userinfo.removeRights & 0x00000200) != 0) { add += 0x00000200; } // No Terminal
            if ((userinfo.removeRights & 0x00000400) != 0) { add += 0x00000400; } // No Files
            if ((userinfo.removeRights & 0x00000010) != 0) { substract += 0x00000010; } // No Console
            if ((userinfo.removeRights & 0x00008000) != 0) { substract += 0x00008000; } // No Uninstall
            if ((userinfo.removeRights & 0x00020000) != 0) { substract += 0x00020000; } // No Remote Command
            if ((userinfo.removeRights & 0x00000040) != 0) { substract += 0x00000040; } // No Wake
            if ((userinfo.removeRights & 0x00040000) != 0) { substract += 0x00040000; } // No Reset/Off
            if (rights != 0xFFFFFFFF) {
                // If not administrator, add and subsctract restrictions
                rights |= add;
                rights &= (0xFFFFFFFF - substract);
            } else {
                // If administrator for a device group, start with permissions and add and subsctract restrictions
                rights = 1 + 2 + 4 + 8 + 32 + 64 + 128 + 16384 + 32768 + 131072 + 262144 + 524288 + 1048576;
                rights |= add;
                rights &= (0xFFFFFFFF - substract);
            }
            return rights;
        }

        // Get the right of a user on a given device group
        function GetMeshRights(mesh, userid) {
            if (mesh == null) { return 0; }
            if (userid == null) { userid = userinfo._id; }
            if (typeof mesh == 'string') { mesh = meshes[mesh] }
            if ((mesh == null) || (mesh.links == null)) { return 0; }

            // Check if super user
            if (serverinfo.manageAllDeviceGroups && (userid == userinfo._id)) return removeUserRights(0xFFFFFFFF, userid);

            // Check device group link permission
            var rights = 0, r = mesh.links[userid];
            if (r != null) {
                if (r.rights == 0xFFFFFFFF) { return removeUserRights(0xFFFFFFFF, userid); } // User has full rights thru a device group link, stop here.
                rights = r.rights;
            }

            // Check permissions thru user groups
            var user = null;
            if (userid == userinfo._id) { user = userinfo; } else { if (users != null) { user = users[userid]; } }
            if (user != null) {
                for (var i in user.links) {
                    if (i.startsWith('ugrp/')) {
                        r = mesh.links[i];
                        if (r != null) {
                            if (r.rights == 0xFFFFFFFF) { return removeUserRights(0xFFFFFFFF, userid); } // User has full rights thru a user group, stop here.
                            rights |= r.rights; // TODO: Deal with reverse permissions
                        }
                    }
                }
            }

            return removeUserRights(rights, userid);
        }

        // Returns true if the user can view the given device group
        function IsMeshViewable(mesh, userid) {
            if (mesh == null) { return false; }
            if (userid == null) { userid = userinfo._id; }
            if (typeof mesh == 'string') { mesh = meshes[mesh] }
            if ((mesh == null) || (mesh.links == null)) { return false; }
            if (mesh.links[userid] != null) { return true; } // User has visilibity thru a direct link

            // Check if user user
            if (serverinfo.manageAllDeviceGroups && (userid == userinfo._id)) return true;

            // Check permissions thru user groups
            var user = null;
            if (userid == userinfo._id) { user = userinfo; } else { if (users != null) { user = users[userid]; } }
            if (user != null) {
                for (var i in user.links) {
                    if ((i.startsWith('ugrp/')) && (mesh.links[i] != null)) { return true; } // User has visilibity thru a user group
                }
            }

            return false;
        }

        // Return the user rights for a given node
        function GetNodeRights(node, userid) {
            if (node == null) { return 0; }
            if (userid == null) { userid = userinfo._id; }
            if (typeof node == 'string') { node = getNodeFromId(node); if (node == null) { return 0; } }
            var r = GetMeshRights(node.meshid, userid);
            if (r == 0xFFFFFFFF) return removeUserRights(r, userid);

            // Check direct device rights using device data
            if ((node.links != null) && (node.links[userid] != null)) { r |= node.links[userid].rights; } // TODO: Deal with reverse permissions

            // Check direct device rights thru user groups
            if ((node.links != null) && (userinfo.links != null)) {
                for (var i in node.links) {
                    if (i.startsWith('ugrp/') && (userinfo.links[i] != null) && (node.links[i].rights != null)) { r |= node.links[i].rights; }
                }
            }

            // Check direct device rights using user data
            /*
            var user = null;
            if (userid == userinfo._id) { user = userinfo; } else { if (users != null) { user = users[userid]; } }
            if ((user != null) && (user.links != null)) {
                var r2 = user.links[node._id];
                if (r2 != null) {
                    if (r2.rights == 0xFFFFFFFF) { return 0xFFFFFFFF; } // User has full rights thru a device link, stop here.
                    r |= r2.rights; // TODO: Deal with reverse permissions
                }
            }
            */
            return removeUserRights(r, userid);
        }

        // Return true if the device is visible to the user
        function IsNodeViewable(node, userid) {
            if (node == null) { return false; }
            if (userid == null) { userid = userinfo._id; }
            if (typeof node == 'string') { node = getNodeFromId(node); if (node == null) { return false; } }
            if (IsMeshViewable(node.meshid, userid)) return true;

            // Check direct device visibility using device data
            if ((node.links != null) && (node.links[userid] != null)) { return true; }

            // Check direct device visibility thru user groups
            if ((node.links != null) && (userinfo.links != null)) {
                for (var i in node.links) { if (i.startsWith('ugrp/') && (userinfo.links[i] != null) && (node.links[i].rights != null)) { return true; } }
            }

            return false;
        }

        //
        // Generic methods
        //

        // Converts string to UTF8 byte array, polyfill for IE.
        // Following method is code from Mozilla: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
        if (typeof TextEncoder === 'undefined') {
            window.TextEncoder = function TextEncoder() { };
            TextEncoder.prototype.encode = function encode(str) {
                'use strict';
                var Len = str.length, resPos = -1;
                var resArr = typeof Uint8Array === 'undefined' ? new Array(Len * 1.5) : new Uint8Array(Len * 3);
                for (var point = 0, nextcode = 0, i = 0; i !== Len;) {
                    point = str.charCodeAt(i), i += 1;
                    if (point >= 0xD800 && point <= 0xDBFF) {
                        if (i === Len) { resArr[resPos += 1] = 0xef; resArr[resPos += 1] = 0xbf; resArr[resPos += 1] = 0xbd; break; }
                        nextcode = str.charCodeAt(i);
                        if (nextcode >= 0xDC00 && nextcode <= 0xDFFF) {
                            point = (point - 0xD800) * 0x400 + nextcode - 0xDC00 + 0x10000;
                            i += 1;
                            if (point > 0xffff) { resArr[resPos += 1] = (0x1e << 3) | (point >>> 18); resArr[resPos += 1] = (0x2 << 6) | ((point >>> 12) & 0x3f); resArr[resPos += 1] = (0x2 << 6) | ((point >>> 6) & 0x3f); resArr[resPos += 1] = (0x2 << 6) | (point & 0x3f); continue; }
                        } else { resArr[resPos += 1] = 0xef; resArr[resPos += 1] = 0xbf; resArr[resPos += 1] = 0xbd; continue; }
                    }
                    if (point <= 0x007f) {
                        resArr[resPos += 1] = (0x0 << 7) | point;
                    } else if (point <= 0x07ff) {
                        resArr[resPos += 1] = (0x6 << 5) | (point >>> 6); resArr[resPos += 1] = (0x2 << 6) | (point & 0x3f);
                    } else {
                        resArr[resPos += 1] = (0xe << 4) | (point >>> 12); resArr[resPos += 1] = (0x2 << 6) | ((point >>> 6) & 0x3f); resArr[resPos += 1] = (0x2 << 6) | (point & 0x3f);
                    }
                }
                if (typeof Uint8Array !== 'undefined') return resArr.subarray(0, resPos + 1);
                resArr.length = resPos + 1;
                return resArr;
            };
            TextEncoder.prototype.toString = function () { return '[object TextEncoder]' };
            try {
                Object.defineProperty(TextEncoder.prototype, 'encoding', {
                    get: function () { if (TextEncoder.prototype.isPrototypeOf(this)) return 'utf-8'; else throw TypeError('Illegal invocation'); }
                });
            } catch (e) { TextEncoder.prototype.encoding = 'utf-8'; }
            if (typeof Symbol !== 'undefined') TextEncoder.prototype[Symbol.toStringTag] = 'TextEncoder';
        }

        // Used to convert Base64 public VAPID key to bytearray.
        function urlBase64ToUint8Array(base64String) {
            var padding = '='.repeat((4 - base64String.length % 4) % 4);
            var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
            var rawData = atob(base64);
            var outputArray = new Uint8Array(rawData.length);
            for (var i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); }
            return outputArray;
        }

        function putstore(name, val) {
            try {
                if ((typeof (localStorage) === 'undefined') || (localStorage.getItem(name) == val)) return;
                if (val == null) { localStorage.removeItem(name); } else { localStorage.setItem(name, val); }
            } catch (ex) { }
            if (name[0] != '_') {
                var s = {};
                try {
                    for (var i = 0, len = localStorage.length; i < len; ++i) {
                        var k = localStorage.key(i);
                        if (k[0] != '_') {
                            s[k] = localStorage.getItem(k);
                            if ((k != 'desktopsettings') && (k != 'stars') && (k != 'deskKeyShortcuts') && (k != 'deskStrings') && (k != 'cmdopt') && (typeof s[k] == 'string') && (s[k].length > 64)) { delete s[k]; }
                        }
                    }
                } catch (ex) { }
                meshserver.send({ action: 'userWebState', state: JSON.stringify(s) });
            }
        }

        // Convert a string into a UTF8 blob with the UTF8 header in front of it.
        function stringToUtf8Blob(str) {
            const bytes = new TextEncoder().encode(str);
            var bytes2 = new Uint8Array(3 + bytes.length);
            bytes2[0] = 0xEF; // This is the UTF-8 header for CSV files, add it to the start of the file.
            bytes2[1] = 0xBB;
            bytes2[2] = 0xBF;
            for (var i = 0; i < bytes.length; i++) { bytes2[i + 3] = bytes[i]; }
            return new Blob([bytes2], { type: 'application/octet-stream' }) // application/json;charset=utf-8
        }

        // Convert a string into a UTF8 blob
        function stringToUtf8BlobNoHeader(str) {
            return new Blob([new TextEncoder().encode(str)], { type: 'application/octet-stream' }) // application/json;charset=utf-8
        }

        function multiTranslate(s) { var i = s.indexOf(']|'); return s.substring(i + 2); } // Used when an English string can have different meanings, so "[MEANING]|word" is used instead as translation key.
        function getLang() { if (navigator.languages != undefined) { return navigator.languages[0]; } return navigator.language; }
        function getNodeAmtVersion(node) { if ((node == null) || (node.intelamt == null) || (typeof node.intelamt.ver != 'string')) return 0; var verSplit = node.intelamt.ver.split('.'); if (verSplit.length < 2) return 0; return parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100); }
        function getstore(name, val) { try { if (typeof (localStorage) === 'undefined') return val; var v = localStorage.getItem(name); if ((v == null) || (v == null)) return val; return v; } catch (e) { return val; } }
        // function addLink(x, f) { return '<span tabindex=0 style=cursor:pointer;text-decoration:none onclick=\'' + f + '\' onkeypress="if (event.key==\'Enter\') {' + f + '} ">' + x + ' <img class=hoverButton src=images/link5.png></span>'; }
        function addLink(x, f) { return '<span tabindex=0 role="button" onclick=\'' + f + '\' onkeypress="if (event.key==\'Enter\') {' + f + '} ">' + x + ' <i class="fa-solid fa-pencil fa-xs"></i></span>'; }
        function addLinkConditional(x, f, c) { if (c) return addLink(x, f); return x; }
        function addKeyLink(x, f) { return '<span tabindex=0 role=button onclick=' + f + ' onkeypress="if (event.key==\'Enter\') { ' + f + ' } ">' + x + ' <i class="fa-solid fa-key"></i></span>'; }
        function addKeyLinkConditional(x, t, c) { if (c) return '<span title=\'' + t + '\'>' + x + ' <i class="fa-solid fa-key"></i></span>'; return x }
        function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
        function addOption(q, t, i) { var option = document.createElement('option'); option.text = t; option.value = i; Q(q).add(option); }
        function passwordcheck(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
        function methodcheck(r) { if (r && r != null && r.Body && r.Body.ReturnValueStr != 'SUCCESS') { messagebox("Error en la Llamada", r.Header.Method + ': ' + r.Body.ReturnValueStr.replace('_', ' ')); return true; } return false; }
        function TableStart() { return '<table cellpadding=0 cellspacing=0 style=width:100%;border-radius:8px><tr><td width=200px><p><td>'; }
        function TableStart2() { return '<table cellpadding=0 cellspacing=0 style=width:100%;border-radius:8px><tr><td><p><td>'; }
        function TableEntry(n, v) { return '<tr><td><p>' + n + '<td>' + v; }
        function FullTable(x, e) { var r = TableStart(); for (i in x) { if (i && x[i]) r += TableEntry(i, x[i]); } return r + TableEnd(e); }
        function TableEnd(n) { return '<tr><td colspan=2><p>' + (n ? n : '') + '</table>'; }
        function AddButton(v, f) { return '<input type=button value="' + v + '" onclick="' + f + '" style=margin:4px>'; }
        function AddButton2(v, f) { return '<input type=button value="' + v + '" onclick="' + f + '">'; }
        function AddRefreshButton(f) { return '<input type=button name=refreshbtn value=Refresh onclick="refreshButtons(false);' + f + '" style=margin:4px ' + (refreshButtonsState == false ? 'disabled' : '') + '>'; }
        function MoreStart() { return '<div id=idx_dlgMoreButtons3 style=display:none><hr>'; };
        function MoreEnd() { return '</div>'; };
        function MoreToggle(v) { QV('idx_dlgMoreButtons1', !v); QV('idx_dlgMoreButtons2', v); QV('idx_dlgMoreButtons3', v); }
        function getSelectedOptions(sel) { var opts = [], opt; for (var i = 0, len = sel.options.length; i < len; i++) { opt = sel.options[i]; if (opt.selected) { opts.push(opt.value); } } return opts; }
        function getInstance(x, y) { for (var i in x) { if (x[i]['InstanceID'] == y) return x[i]; } return null; }
        function getItem(x, y, z) { for (var i in x) { if (x[i][y] == z) return x[i]; } return null; }
        function guidToStr(g) { return g.substring(6, 8) + g.substring(4, 6) + g.substring(2, 4) + g.substring(0, 2) + '-' + g.substring(10, 12) + g.substring(8, 10) + '-' + g.substring(14, 16) + g.substring(12, 14) + '-' + g.substring(16, 20) + '-' + g.substring(20); }
        function getUrlVars() { var j, hash, vars = [], hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); for (var i = 0; i < hashes.length; i++) { j = hashes[i].indexOf('='); if (j > 0) { vars[hashes[i].substring(0, j)] = hashes[i].substring(j + 1, hashes[i].length); } } return vars; }
        //function getDocWidth() { if (window.innerWidth) return window.innerWidth; if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientWidth != 0) return document.documentElement.clientWidth; return document.getElementsByTagName('body')[0].clientWidth; }
        //function addHtmlValue(t, v) { return '<div style=height:20px><div style=float:right;width:220px><b>' + v + '</b></div><div>' + t + '</div></div>'; }
        function addHtmlValue(t, v, classNames = []) { return `<div class="row mb-1"><div class="${classNames[0] || 'col col-md-2 col-lg-2'}"><b>${t}</b></div><div class="${classNames[1] || 'col col-md-10 col-lg-10'}">${v}</div></div>`; }
        function addHtmlFormFloating(t, v, classNames = []) { return `<div class="row mb-1"><div class="form-floating mb-3 ${classNames[0] || 'col-md-12'}">${v}<label class="${classNames[1] || 'ms-2'}">${t}</label></div></div>`; }


        function addHtmlValue2(t, v) { return '<div><div style=display:inline-block;float:right>' + v + '</div><div style=display:inline-block>' + t + '</div></div>'; }
        function addHtmlValue3(t, v) { return '<div><b>' + t + '</b></div><div style=margin-left:16px>' + v + '</div>'; }
        function addHtmlValue4(t, v) { return '<table style=width:100%><td style=width:120px>' + t + '<td style=text-align:right><b>' + v + '</b></table>'; }
        function addHtmlValue5(t, v) { return '<div style=padding:4px><div style=display:inline-block;float:right><b>' + v + '</b></div><div style=display:inline-block>' + t + '</div></div>'; }
        function focusTextBox(x) { setTimeout(function () { Q(x).selectionStart = Q(x).selectionEnd = 65535; Q(x).focus(); }, 0); }
        function validateEmail(v) { var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(v); } // New version
        function isPrivateIP(a) { return (a.startsWith('10.') || a.startsWith('172.16.') || a.startsWith('192.168.')); }
        function u2fSupported() { return (window.u2f && ((navigator.userAgent.indexOf('Chrome/') > 0) || (navigator.userAgent.indexOf('Firefox/') > 0) || (navigator.userAgent.indexOf('Opera/') > 0) || (navigator.userAgent.indexOf('Safari/') > 0))); }
        function findOne(arr1, arr2) { if ((arr1 == null) || (arr2 == null)) return false; return arr2.some(function (v) { return arr1.indexOf(v) >= 0; }); };
        function copyTextToClip(txt) { function selectElementText(e) { if (document.selection) { var range = document.body.createTextRange(); range.moveToElementText(e); range.select(); } else if (window.getSelection) { var range = document.createRange(); range.selectNode(e); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); } } var e = document.createElement('DIV'); e.textContent = txt; document.body.appendChild(e); selectElementText(e); document.execCommand('copy'); e.remove(); }
        function copyTextToClip2(txt) { function selectElementText(e) { if (document.selection) { var range = document.body.createTextRange(); range.moveToElementText(e); range.select(); } else if (window.getSelection) { var range = document.createRange(); range.selectNode(e); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); } } var e = document.createElement('DIV'); e.textContent = decodeURIComponent(txt); document.body.appendChild(e); selectElementText(e); document.execCommand('copy'); e.remove(); }
        function capitalizeFirstLetter(x) { return x.charAt(0).toUpperCase() + x.slice(1); }
        function printDate(d) { return d.toLocaleDateString(args.locale); }
        function printTime(d) { return d.toLocaleTimeString(args.locale); }
        function printDateTime(d) { return d.toLocaleString(args.locale); }
        function printTimer(d) { return zeroPad(Math.floor(d / 3600), 2) + ':' + zeroPad((Math.floor(d / 60) % 60), 2) + ':' + zeroPad((d % 60), 2); }
        function printFlexDateTime(d) { if (printDate(new Date()) == printDate(d)) { return printTime(d); } else { return printDateTime(d); } }

        function addDetailItem(title, value, state, classNames = []) {
            return `<div class="row mb-1"><div class="${classNames[0] || 'col-md-7'}"> ${nobreak(title)} </div><div class="${classNames[1] || 'col-md-5 text-end'}"> ${value} </div></div>`;
        }

        function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
        function addTextLink(subtext, text, link) { var i = text.toLowerCase().indexOf(subtext.toLowerCase()); if (i == -1) { return text; } return text.substring(0, i) + '<a href="' + link + '">' + subtext + '</a>' + text.substring(i + subtext.length); }
        function getOrderedList(objList, oname) { var r = []; for (var i in objList) { r.push(objList[i]); } r.sort(function (a, b) { var aa = a[oname].toLowerCase(), bb = b[oname].toLowerCase(); if (aa > bb) return 1; if (aa < bb) return -1; return 0; }); return r; }
        function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); }
        function nobreak(x) { return x.split(' ').join('&nbsp;'); }
        function pad2(num) { var s = '00' + num; return s.substr(s.length - 2); }
        function encodeURIComponentEx(txt) { return encodeURIComponent(txt).replace(/'/g, '%27'); };
        function getUserName(userid) {
            var useridsplit = userid.split('/'), userid2 = useridsplit[0] + '/' + useridsplit[1] + '/' + useridsplit[2], guestname = '';
            if ((useridsplit.length == 4) && (useridsplit[3].startsWith('guest:'))) { guestname = ' - ' + decode_utf8(atob(useridsplit[3].substring(6))); }
            if (users && users[userid2] != null) { if (users[userid2].realname != null) return (users[userid2].realname + guestname); else return (users[userid2].name + guestname); }
            if (currentNode && currentNode.links && currentNode.links[userid] && currentNode.links[userid].name != null) { return (currentNode.links[userid].name + guestname); }
            if (userid == userinfo._id) { return (userinfo.name + guestname); }
            if (nodes) { for (var a in nodes) { if (nodes[a].links) { for (var b in nodes[a].links) { if (nodes[a].links[b].name && b == userid) return (nodes[a].links[b].name + guestname); } } } }
            if (meshes) { for (var a in meshes) { if (meshes[a].links) { for (var b in meshes[a].links) { if (meshes[a].links[b].name && b == userid) return (meshes[a].links[b].name + guestname); } } } }
            return (useridsplit[2] + guestname);
        }
        function round(value, precision) { var multiplier = Math.pow(10, precision || 0); return Math.round(value * multiplier) / multiplier; }
        function safeNewWindow(url, target) { var newWindow = window.open(url, target, 'noopener,noreferrer'); if (newWindow) { newWindow.opener = null; } }
        function isWindowsNode(node) { if ((node.mtype != 2) || (node.agent == null) || (node.agent.id == null)) return false; return ([1, 2, 3, 4, 21, 22, 34, 42, 43].indexOf(node.agent.id) >= 0); }
        function isMacNode(node) { if ((node.mtype != 2) || (node.agent == null) || (node.agent.id == null)) return false; return ([11,16,29].indexOf(node.agent.id) >= 0); }

        function downloadFile(link, name, closeDialog) {
            var element = document.createElement('iframe');
            element.style.cssText = 'display:none;width:0;height:0;border:0;visibility:hidden;';
            element.src = link;
            document.body.appendChild(element);
            setTimeout(function() { try { if (element.parentNode) { document.body.removeChild(element); } } catch(e) { } }, 10000);
            if (closeDialog) { setDialogMode(0); }
        }

        // Make the dialog box movable
        function dialogBoxDrag() {
            var elmnt = Q('xxAddAgentModal');
            var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
            Q('dialogHeader').onmousedown = dragMouseDown;
            function dragMouseDown(e) {
                e = e || window.event;
                e.preventDefault();
                pos3 = e.clientX;
                pos4 = e.clientY;
                document.onmouseup = closeDragElement;
                document.onmousemove = elementDrag;
            }
            function elementDrag(e) {
                e = e || window.event;
                e.preventDefault();
                pos1 = pos3 - e.clientX;
                pos2 = pos4 - e.clientY;
                pos3 = e.clientX;
                pos4 = e.clientY;
                elmnt.style.top = (elmnt.offsetTop - pos2) + 'px';
                elmnt.style.left = (elmnt.offsetLeft - pos1) + 'px';
            }
            function closeDragElement() {
                document.onmouseup = null;
                document.onmousemove = null;
            }
        }

        // --- Icons Customization ---
        var customIconValues = {};
        const customIconConfig = [
            { key: 'myDevices', label: 'My Devices', elementId: 'LeftMenuMyDevices' },
            { key: 'myAccount', label: 'My Account', elementId: 'LeftMenuMyAccount' },
            { key: 'myEvents', label: 'My Events', elementId: 'LeftMenuMyEvents' },
            { key: 'myFiles', label: 'My Files', elementId: 'LeftMenuMyFiles' },
            { key: 'myUsers', label: 'My Users', elementId: 'LeftMenuMyUsers' },
            { key: 'myServer', label: 'My Server', elementId: 'LeftMenuMyServer' }
        ];

        function getVisibleCustomIconConfig() {
            var visibleConfig = [];
            for (var i = 0; i < customIconConfig.length; i++) {
                var cfg = customIconConfig[i];
                var anchor = document.getElementById(cfg.elementId);
                if (anchor == null) { continue; }
                if (window.getComputedStyle(anchor).display === 'none') { continue; }
                visibleConfig.push(cfg);
            }
            return visibleConfig;
        }

        function loadCustomIconState() {
            var raw = getstore('customIcons', '{}');
            if ((typeof raw !== 'string') || (raw.length === 0)) { return {}; }
            try { return JSON.parse(raw); } catch (ex) { return {}; }
        }

        function normalizeCustomIconPath(value) {
            if (typeof value !== 'string') { return value; }
            var trimmed = value.trim();
            if (trimmed.length === 0) { return trimmed; }
            var lower = trimmed.toLowerCase();
            if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) { return trimmed; }
            if ((typeof domainUrl === 'string') && (domainUrl !== '/') && (domainUrl.length > 0)) {
                if (trimmed.startsWith('/icons/custom/')) {
                    return domainUrl + trimmed.substring(1);
                }
                if (trimmed.startsWith('icons/custom/')) {
                    return domainUrl + trimmed;
                }
            }
            if (trimmed.startsWith('icons/custom/')) { return '/' + trimmed; }
            return trimmed;
        }

        function sanitizeCustomIconState(state) {
            var sanitized = {};
            if (state == null) { return sanitized; }
            for (var i = 0; i < customIconConfig.length; i++) {
                var key = customIconConfig[i].key;
                var value = state[key];
                if (typeof value === 'string') {
                    var trimmed = normalizeCustomIconPath(value.trim());
                    if (trimmed.length > 0) { sanitized[key] = trimmed; }
                }
            }
            return sanitized;
        }

        function persistCustomIconState(state) {
            var sanitized = sanitizeCustomIconState(state);
            putstore('customIcons', JSON.stringify(sanitized));
            customIconValues = sanitized;
            applyIconCustomization(sanitized);
        }

        function showIconCustomization() {
            customIconValues = loadCustomIconState();
            var visibleIconConfig = getVisibleCustomIconConfig();
            var x = '<div class="container-fluid">';
            x += '<div class="row mb-4">';
            x += '<div class="col-12">';
            x += '<div class="d-flex align-items-center mb-3">';
            x += '<div class="bg-primary bg-gradient rounded-circle p-3 me-3">';
            x += '<i class="fas fa-palette fa-2x text-white"></i>';
            x += '</div>';
            x += '<div>';
            x += '<h5 class="mb-1 fw-semibold">Customize Your Sidebar Icons</h5>';
            x += '<p class="text-muted mb-0">Upload custom SVG, PNG or JPEG icons up to 10 MB, or provide URLs to personalize your sidebar interface experience</p>';
            x += '</div>';
            x += '</div>';
            x += '</div>';
            x += '</div>';

            x += '<div class="row" id="iconCardsContainer">';
            for (var i = 0; i < visibleIconConfig.length; i++) {
                var cfg = visibleIconConfig[i];
                var currentValue = customIconValues[cfg.key] || '';
                var hasIcon = currentValue.length > 0;

                x += '<div class="col-lg-6 col-xl-4 mb-3">';
                x += '<div class="card modern-card h-100 ' + (hasIcon ? 'border-success' : '') + '" data-icon-key="' + cfg.key + '">';
                x += '<div class="card-header d-flex align-items-center">';
                x += '<div class="bg-light rounded-circle p-2 me-3">';
                x += '<i class="fas ' + getIconClass(cfg.key) + ' fa-lg text-secondary"></i>';
                x += '</div>';
                x += '<div class="flex-grow-1">';
                x += '<h6 class="card-title mb-1">' + EscapeHtml(cfg.label) + '</h6>';
                x += '<small class="status-badge ' + (hasIcon ? 'text-success' : 'text-muted') + '">';
                x += '<i class="fas ' + (hasIcon ? 'fa-check-circle' : 'fa-circle') + ' me-1"></i>';
                x += '<span class="status-text">' + (hasIcon ? 'Custom icon set' : 'Default icon') + '</span>';
                x += '</small>';
                x += '</div>';
                x += '</div>';

                x += '<div class="card-body">';
                x += '<div class="icon-upload-wrapper" data-icon-key="' + cfg.key + '"></div>';
                x += '</div>';
                x += '</div>';
                x += '</div>';
            }
            if (visibleIconConfig.length === 0) {
                x += '<div class="col-12"><div class="alert alert-secondary mb-0">No sidebar icons are available for customization on this account.</div></div>';
            }
            x += '</div>';
            x += '</div>';

            openModal({
                modalId: 'xxAddAgent',
                title: 'Icons Customization',
                body: x,
                size: 'large',
                okButtonId: 'idx_dlgOkButton',
                onOk: saveIconCustomization
            });

            // Initialize icon upload components after modal is shown
            setTimeout(function() {
                initializeIconUploadComponents();
            }, 100);

            return false;
        }

        function initializeIconUploadComponents() {
            var visibleIconConfig = getVisibleCustomIconConfig();
            for (var i = 0; i < visibleIconConfig.length; i++) {
                var cfg = visibleIconConfig[i];
                var currentValue = customIconValues[cfg.key] || '';
                var wrapper = document.querySelector('[data-icon-key="' + cfg.key + '"] .icon-upload-wrapper');

                if (wrapper) {
                    createIconUploadComponent(cfg.key, wrapper, {
                        label: cfg.label,
                        currentValue: currentValue,
                        onUpload: uploadAndPersistCustomIcon,
                        onUrlInput: handleUrlInput,
                        onRemove: removeCustomIcon,
                        normalizePreviewUrl: normalizeCustomIconPath
                    });
                }
            }
        }

        // Upload via API, then persist local icon state so sidebar updates immediately.
        async function uploadAndPersistCustomIcon(iconKey, file) {
            var result = await uploadCustomIcon(iconKey, file);
            if ((result != null) && (typeof result.path === 'string') && (result.path.length > 0)) {
                customIconValues[iconKey] = result.path;
                persistCustomIconState(customIconValues);
                setIconCardStatus(iconKey, 'custom');
            }
            return result;
        }

        function setIconCardStatus(iconKey, state) {
            var card = document.querySelector('[data-icon-key="' + iconKey + '"]');
            if (!card) { return; }
            var statusElement = card.querySelector('.card-header small');
            card.classList.remove('border-warning', 'border-success');
            if (state === 'custom') {
                if (statusElement) {
                    statusElement.className = 'text-success';
                    statusElement.innerHTML = '<i class="fas fa-check-circle me-1"></i>Custom icon set';
                }
                card.classList.add('border-success');
            } else if (state === 'unsaved') {
                if (statusElement) {
                    statusElement.className = 'text-warning';
                    statusElement.innerHTML = '<i class="fas fa-exclamation-circle me-1"></i>Unsaved changes';
                }
                card.classList.add('border-warning');
            } else {
                if (statusElement) {
                    statusElement.className = 'text-muted';
                    statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>Default icon';
                }
            }
        }

        function getIconClass(iconKey) {
            const iconMap = {
                'myDevices': 'fa-desktop',
                'myAccount': 'fa-user-circle',
                'myEvents': 'fa-calendar-alt',
                'myFiles': 'fa-folder',
                'myUsers': 'fa-users',
                'myServer': 'fa-server'
            };
            return iconMap[iconKey] || 'fa-icons';
        }

        function handleUrlInput(iconKey, inputOrValue) {
            var value = '';
            if (typeof inputOrValue === 'string') { value = inputOrValue.trim(); }
            else if (inputOrValue && (typeof inputOrValue.value === 'string')) { value = inputOrValue.value.trim(); }
            // Preview rendering is handled by IconUploadComponent.
            setIconCardStatus(iconKey, (value.length > 0) ? 'unsaved' : 'default');
        }

        function removeCustomIcon(iconKey) {
            var previousValue = (customIconValues && (typeof customIconValues[iconKey] === 'string')) ? customIconValues[iconKey] : '';
            customIconValues[iconKey] = '';
            var textInput = document.getElementById('iconInput_' + iconKey);
            if (textInput) { textInput.value = ''; }
            persistCustomIconState(customIconValues);

            if ((typeof previousValue === 'string') && (previousValue.indexOf('/icons/custom/') >= 0)) {
                deleteCustomIconFromServer(previousValue).catch(function (ex) {
                    // Keep UX non-blocking: icon is already reset locally.
                    if (window.console) { console.error('Failed to delete custom icon:', ex); }
                });
            }

            setIconCardStatus(iconKey, 'default');
            return false;
        }

        async function uploadCustomIcon(iconKey, file) {
            var formData = new FormData();
            formData.append('iconType', iconKey);
            if (customIconValues && typeof customIconValues[iconKey] === 'string' && customIconValues[iconKey].length > 0) {
                formData.append('previousIcon', customIconValues[iconKey]);
            }
            formData.append('iconFile', file);

            var response = await fetch('customiconupload.ashx', { method: 'POST', body: formData, credentials: 'same-origin' });
            if (!response.ok) {
                var message = 'Failed to upload the icon.';
                try {
                    var errorInfo = await response.json();
                    if (errorInfo && typeof errorInfo.error === 'string' && errorInfo.error.length > 0) { message = errorInfo.error; }
                } catch (ex) { }
                throw new Error(message);
            }
            return response.json();
        }

        async function deleteCustomIconFromServer(iconPath) {
            var body = new URLSearchParams();
            body.append('iconPath', iconPath);
            var response = await fetch('customicondelete.ashx', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
                body: body.toString(),
                credentials: 'same-origin'
            });
            if (!response.ok) {
                var message = 'Failed to delete custom icon.';
                try {
                    var errorInfo = await response.json();
                    if (errorInfo && typeof errorInfo.error === 'string' && errorInfo.error.length > 0) { message = errorInfo.error; }
                } catch (ex) { }
                throw new Error(message);
            }
            return response.json();
        }

        function saveIconCustomization() {
            var updatedState = loadCustomIconState();
            var visibleIconConfig = getVisibleCustomIconConfig();
            for (var i = 0; i < visibleIconConfig.length; i++) {
                var cfg = visibleIconConfig[i];
                var input = document.getElementById('iconInput_' + cfg.key);
                if (input) { updatedState[cfg.key] = input.value; }
            }
            persistCustomIconState(updatedState);
            if (xxModal) { xxModal.hide(); }
        }

        // Supports existing SVG menu elements and modern FontAwesome <i> icons.
        function applyIconCustomization(icons) {
            var state = sanitizeCustomIconState(icons);
            for (var i = 0; i < customIconConfig.length; i++) {
                var cfg = customIconConfig[i];
                var anchor = document.getElementById(cfg.elementId);
                if (!anchor) { continue; }
                var iconElement = anchor.querySelector('i,svg');
                if (!iconElement) { continue; }
                if (state[cfg.key]) {
                    anchor.classList.add('custom-icon');
                    iconElement.style.setProperty('background-image', 'url("' + state[cfg.key].replace(/"/g, '%22') + '")', 'important');
                    iconElement.style.setProperty('background-repeat', 'no-repeat', 'important');
                    iconElement.style.setProperty('background-position', 'center', 'important');
                    iconElement.style.setProperty('background-size', 'contain', 'important');
                    if (iconElement.tagName.toLowerCase() === 'i') {
                        // Font Awesome icons are pseudo-elements; hide glyph and show background image.
                        iconElement.style.setProperty('font-size', '0', 'important');
                        iconElement.style.setProperty('color', 'transparent', 'important');
                        iconElement.style.setProperty('width', '1.1em', 'important');
                        iconElement.style.setProperty('height', '1.1em', 'important');
                        iconElement.style.setProperty('display', 'inline-block', 'important');
                        iconElement.style.setProperty('vertical-align', 'middle', 'important');
                    }
                } else {
                    anchor.classList.remove('custom-icon');
                    iconElement.style.removeProperty('background-image');
                    iconElement.style.removeProperty('background-repeat');
                    iconElement.style.removeProperty('background-position');
                    iconElement.style.removeProperty('background-size');
                    if (iconElement.tagName.toLowerCase() === 'i') {
                        iconElement.style.removeProperty('font-size');
                        iconElement.style.removeProperty('color');
                        iconElement.style.removeProperty('width');
                        iconElement.style.removeProperty('height');
                        iconElement.style.removeProperty('display');
                        iconElement.style.removeProperty('vertical-align');
                    }
                }
            }
        }

        document.addEventListener('DOMContentLoaded', function () {
            customIconValues = loadCustomIconState();
            applyIconCustomization(customIconValues);
        });

        window.addEventListener('load', function () {
            applyIconCustomization(customIconValues);
        });

        // Request Confirmation if closing while a desktop, terminal session is active
        window.addEventListener('beforeunload', function (e) {
            if (((desktop != null) && (xxcurrentView == 11)) || ((terminal != null) && (xxcurrentView == 12))) { e.preventDefault(); e.returnValue = ''; }
        });
    </script>



</body></html>