<!DOCTYPE html><html lang="ca" 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ó</b></div>
        <div id="cxdesktop" class="cmtext" onclick="cmaction(3,event)">Escriptori</div>
        <div id="cxterminal" class="cmtext" onclick="cmaction(2,event)">Terminal</div>
        <div id="cxfiles" class="cmtext" onclick="cmaction(4,event)">Fitxers</div>
        <div id="cxevents" class="cmtext" onclick="cmaction(5,event)">Esdeveniments</div>
        <div id="cxdetails" class="cmtext" onclick="cmaction(6,event)">Detalls</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">Connectors</div>
        <hr id="cxmgroupsplit">
        <div id="cxstar" class="cmtext" onclick="cmaction(10,event)" style="display:none">Alterna Estrella</div>
        <div id="cxmdesktop" class="cmtext" onclick="cmaction(9,event)" style="display:none">Multi-escriptori</div>
    </div>
    <div id="meshContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div id="cxselectall" class="cmtext" onclick="cmmeshaction(1,event)">Seleccionar tot</div>
        <div id="cxselectnone" class="cmtext" onclick="cmmeshaction(2,event)">Seleccioneu Cap</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)">Port alternatiu</div>
    </div>
    <div id="sshPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmsshportaction(1,event)">Port alternatiu</div>
    </div>
    <div id="rfbPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmrfbportaction(1,event)">Port alternatiu</div>
    </div>
    <div id="httpPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpportaction(1,event)">Port alternatiu</div>
    </div>
    <div id="httpsPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpsportaction(1,event)">Port alternatiu</div>
    </div>
    <div id="filesContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmfilesaction(1,event)">Canvia el nom</div>
        <div class="cmtext" onclick="cmfilesaction(2,event)">Edita</div>
        <div class="cmtext" onclick="cmfilesaction(3,event)">Suprimeix</div>
    </div>
    <div id="expandAllContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmexpandaction(1,event)">Amplia-ho tot</div>
        <div class="cmtext" onclick="cmexpandaction(2,event)">Col · lapsar tot</div>
    </div>
    <div id="deskPlayerContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskplayeraction(1,event)">Obre el reproductor...</div>
    </div>
    <div id="deskKeyShortcutContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskshortcutaction(1,event)">Personalitza...</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)">Personalitza...</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="Feu clic per veure les notificacions actuals" 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>Els meus dispositius</span>
                                </div>

                                <div class="userDropdownMenuItem userDropdownMobileOnly mobile-menu-item" data-action="events" onclick="handleUserMenuItem('events')">
                                    <i class="fa fa-calendar userDropdownMenuIcon"></i>
                                    <span>Els meus esdeveniments</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>Els meus usuaris</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>Els meus fitxers</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>El meu 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>UI Settings</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>Toggle Modern UI</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>Left Bar Interface</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="top-bar" onclick="handleUserMenuItem('top-bar')">
                                        <i class="fa fa-window-maximize userDropdownMenuIcon"></i>
                                        <span>Top Bar Interface</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="fixed-width" onclick="handleUserMenuItem('fixed-width')">
                                        <i class="fa fa-columns userDropdownMenuIcon"></i>
                                        <span>Fixed Width Interface</span>
                                    </div>
                                    <div id="toggleFooterMenuItem" class="userDropdownMenuItem" data-action="toggle-footer" onclick="handleUserMenuItem('toggle-footer')">
                                        <i class="fa fa-window-minimize userDropdownMenuIcon"></i>
                                        <span>Toggle Footer Bar</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">Canvia el mode nocturn</span>
                                </div>


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

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

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

                                <div class="userDropdownMenuItem" data-action="logout" onclick="handleUserMenuItem('logout')">
                                    <i class="fa fa-sign-out userDropdownMenuIcon"></i>
                                    <span>Tancar sessió</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)">Els meus dispositius</td>
                            <td tabindex="0" id="MainMenuMyAccount" class="topbar_td style3x" onmouseup="go(2,event)" onkeypress="if (event.key == 'Enter') go(2)">El meu compte</td>
                            <td tabindex="0" id="MainMenuMyEvents" class="topbar_td style3x" onmouseup="go(3,event)" onkeypress="if (event.key == 'Enter') go(3)">Els meus esdeveniments</td>
                            <td tabindex="0" id="MainMenuMyFiles" class="topbar_td style3x" onmouseup="go(5,event)" onkeypress="if (event.key == 'Enter') go(5)">Els meus fitxers</td>
                            <td tabindex="0" id="MainMenuMyUsers" class="topbar_td style3x" onmouseup="go(4,event)" onkeypress="if (event.key == 'Enter') go(4)">Els meus usuaris</td>
                            <td tabindex="0" id="MainMenuMyServer" class="topbar_td style3x" onmouseup="go(6,event)" onkeypress="if (event.key == 'Enter') go(6)">El meu 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)">Escriptori</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)">Fitxers</td>
                                        <td tabindex="0" id="MainDevEvents" class="topbar_td style3x" onmouseup="go(16,event)" onkeypress="if (event.key == 'Enter') go(16)">Esdeveniments</td>
                                        <td tabindex="0" id="MainDevInfo" class="topbar_td style3x" onmouseup="go(17,event)" onkeypress="if (event.key == 'Enter') go(17)">Detalls</td>
                                        <td tabindex="0" id="MainDevApps" class="topbar_td style3x" onmouseup="go(18,event)" onkeypress="if (event.key == 'Enter') go(18)">Programari</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)">Connectors</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)">Resum</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)">Esdeveniments</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)">Esdeveniments</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)">Usuaris</td>
                                <td tabindex="0" id="UsersGroups" class="topbar_td style3x" onmouseup="go(50,event)" onkeypress="if (event.key == 'Enter') go(50)">Grups</td>
                                <td tabindex="0" id="UsersRecordings" class="topbar_td style3x" onmouseup="go(52,event)" onkeypress="if (event.key == 'Enter') go(52)">Enregistraments</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ístiques</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)">Traça</td>
                                <td tabindex="0" id="ServerPlugins" class="topbar_td style3x" onmouseup="go(42,event)" onkeypress="if (event.key == 'Enter') go(42)">Connectors</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 desconnectat</span>, <href onclick="reload()" style="cursor:pointer"><u>feu clic per tornar a connectar</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">Els meus dispositius</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="Aneu a la primera pàgina" 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="Anar a la pàgina 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="Aneu a la pàgina següent" 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="Anar a l'última pàgina" 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="Columnes">
                            <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="Llista">
                            <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="Escriptoris">
                            <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="Escriptoris">
                            <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="Col · lapsar tot" 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="Amplia-ho tot" 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 tot">
                        <input type="button" class="btn btn-primary me-1 btn-sm" id="GroupActionButton" disabled="disabled" value="Acció grupal" onclick="groupActionFunction()">
                        <input type="button" class="btn btn-secondary me-1 btn-sm" id="ScrollToTopButton" onclick="onDevicesScroll(true);" value="Desplaceu-vos cap a dalt">
                        <input type="text" id="SearchInput" class="form-control-sm me-1 btn-sm" placeholder="Filtre" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Filtra dispositius per nom o prefix: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Prefix amb ! per negar, p.ex. !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="Filtre de dispositiu">
                            <option value="0" selected="">Tots</option>
                            <option value="1">En línia</option>
                            <option value="5">Fora de línia</option>
                            <option value="2">Sessions</option>
                            <option value="3">Destacat</option>
                            <option value="4">Intel® AMT</option>
                            <option value="6">Ajuda</option>
                            <option value="7">Etiquetat</option>
                            <option value="8">Sense etiquetar</option>
                        </select>
                        <div class="form-check" title="Mostra el nom del sistema operatiu dels dispositius"><input class="form-check-input me-1" type="checkbox" id="RealNameCheckBox" onclick="onRealNameCheckBox()"><label class="form-check-label" for="RealNameCheckBox">Nom del sistema operatiu</label></div>
                        <label style="display:none"><input type="checkbox" id="OnlineCheckBox" class="form-check-input me-2" onclick="onOnlineCheckBox(event)"><span title="Mostra només els dispositius que estan en línia">En línia</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="Col · lapsar tot" 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="Amplia-ho tot" 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="Connecta-ho tot">
                            <input type="button" class="btn btn-danger me-1 btn-sm" onclick="disconnectAllKvmFunction()" value="Desconnectar-ho tot">
                            <div class="form-check form-check-inline me-1 align-middle" title="Connexió automàtica">
                                <input class="form-check-input me-1" type="checkbox" id="autoConnectDesktopCheckbox" onclick="autoConnectDesktops(event)">
                                <label class="form-check-label" for="autoConnectDesktopCheckbox">Automàtic</label>
                            </div>
                        </span>
                        <input type="button" class="btn btn-secondary me-1 btn-sm" onclick="onDevicesScroll(true);" value="Desplaceu-vos cap a dalt">
                        <input type="button" class="btn btn-primary me-1 btn-sm" onclick="showMultiDesktopSettings()" value="Configuració">
                        <input type="text" id="KvmSearchInput" class="form-control-sm me-1" placeholder="Filtre" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Filtra dispositius per nom o prefix: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Prefix amb ! per negar, p.ex. !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="Cerca la ubicació" onfocus="onMapSearchFocus(1)" onblur="onMapSearchFocus(0)">
                        <input class="btn btn-primary me-1 btn-sm" type="button" value="Cerca" title="Cerca la ubicació" onclick="getSearchLocation()">
                        <input class="btn btn-secondary me-1 btn-sm" type="button" id="refreshmap" title="Restableix la vista del mapa" value="Restableix" onclick="refreshMap(false,true)">
                    </div>
                    <div class="d-flex align-items-center ms-auto">
                        <div style="display:none" id="devListToolbarView">
                            Veure
                            <select id="viewselect" onchange="onDeviceViewChange()">
                                <option value="1">Columnes</option>
                                <option value="2">Llista</option>
                                <option value="3">Escriptoris d'amplada fixa</option>
                                <option id="viewselectmapoption" value="4" style="display:none">Mapa</option>
                                <option value="5">Escriptoris</option>
                            </select>
                        </div>
                        <div style="display:none" id="devListToolbarSize">
                            Mida
                            <select id="sizeselect" class="form-select-sm me-2" onchange="onDeviceViewChange()">
                                <option value="0">Petit</option>
                                <option value="1">Mitjana</option>
                                <option value="2">Gran</option>
                            </select>
                            &nbsp;
                        </div>
                        <div style="display:none" id="devListToolbarSort">
                            Ordena
                            <select id="sortselect" class="form-select-sm me-2" onchange="mainUpdate(6)">
                                <option>Grup</option>
                                <option>Poder</option>
                                <option>Dispositiu</option>
                                <option>Etiquetes</option>
                                <option>Grup-Etiquetes</option>
                                <option>Vist per últim cop</option>
                                <option>Última hora d'arrencada</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">Per començar, <a href="#" onclick="return account_createMesh()"><strong>feu clic aquí per crear un grup de dispositius</strong></a>.</div>
                                <div id="getStarted2">No hi ha grups de dispositius.</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">Resultats d'ubicació</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>El meu compte</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)">Canvia imatge</a></div>
                    </div>
                    <div id="p2AccountSecurity" style="display:none">
                        <p><strong>Seguretat del compte</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()">Gestionar el número de telèfon</a><br></span>
                            </div>
                            <div id="manageEmail2FA">
                                <div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthEmail()">Gestioneu l'autenticació del correu electrònic</a><br></span>
                            </div>
                            <div id="manageAuthApp">
                                <div class="p2AccountActions"><span id="authAppSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthApp()">Gestioneu l'aplicació d'autenticació</a><br></span>
                            </div>
                             <div id="manageDuoApp">
                                <div class="p2AccountActions"><span id="authDuoSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthDuo()">Gestiona l'autenticació 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)">Gestionar les claus de seguretat</a><br></span>
                            </div>
                            <div id="managePushAuthDev">
                                <div class="p2AccountActions"><span id="authPushAuthDevCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_managePushAuthDev()">Gestioneu l'autenticació 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()">Gestiona la missatgeria</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)">Gestioneu els codis de seguretat</a><br></span>
                            </div>
                            <div class="p2AccountActions"></div><span><a href="#" onclick="return account_viewPreviousLogins()">Veure els inicis de sessió anteriors</a><br></span>
                        </div>
                    </div>
                    <div id="p2AccountActions">
                        <p><strong>Accions del compte</strong></p>
                        <p class="mL">
                            <span id="managePhoneNumber2" style="display:none"><a href="#" onclick="return account_managePhone()">Gestionar el número de telèfon</a><br></span>
                            <span id="manageMessaging2" style="display:none"><a href="#" onclick="return account_manageMessaging()">Gestiona la missatgeria</a><br></span>
                            <span id="verifyEmailId" style="display:none"><a href="#" onclick="return account_showVerifyEmail()">Verificar Correu</a><br></span>
                            <span id="accountEnableNotificationsSpan" style="display:none"><a href="#" onclick="return account_enableNotifications()">Activa les notificacions web</a><br></span>
                            <a href="#" onclick="return account_showLocalizationSettings()">Configuració de localització</a><br>
                            <a href="#" onclick="return account_showAccountNotifySettings()">Configuració de notificacions</a><br>
                            <span id="p2AccountPassActions">
                                <span id="accountChangeEmailAddressSpan" style="display:none"><a href="#" onclick="return account_showChangeEmail()">Canvia l'adreça de correu electrònic</a><br></span>
                                <a href="#" onclick="return account_showChangePassword()">Canvia la contrasenya</a><span id="p2nextPasswordUpdateTime"></span><br>
                                <a href="#" onclick="return account_showDeleteAccount()">Esborrar compte</a><br>
                            </span>
                            <span id="accountCreateLoginTokenSpan" style="display:none"><a href="#" onclick="return account_createLoginToken()">Crea un testimoni d'inici de sessió</a><br></span>
                            <a href="#" onclick="return account_showThemesSwitcher()">Canvia el tema</a><br>
                            <span id="accountCustomIconsSpan" style="display:none"><a href="#" onclick="return showIconCustomization()">Personalització d'icones</a><br></span>
                        </p>
                    </div>
                    <div id="p2logintokens"></div>
                    <strong>Grups de dispositius</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> Nou</button></span>
                    <br><br>
                    <div id="p2meshes"></div>
                    <div id="p2noMeshFound" style="display:none">No hi ha grups de dispositius.<span id="p2createMeshLink2"> <a href="#" onclick="return account_createMesh()"><strong>Comença 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">Els meus esdeveniments</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">Filtre</label>
                                <select id="p3filterevents" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="">Tots els registres</option>
                                    <option notransval="1" value="agentlog">Registres d'agents</option>
                                    <option notransval="1" value="relaylog">Registres de relleus</option>
                                    <option notransval="1" value="manual">Registres manuals</option>
                                    <option notransval="1" value="runcommands">Executeu registres d'ordres</option>
                                    <option notransval="1" value="batchupload">Registres de càrrega per lots</option>
                                    <option notransval="1" value="changenode">Canvia els registres de nodes</option>
                                    <option notransval="1" value="removenode">Elimina els registres de nodes</option>
                                </select>
                                <label for="p3limitdropdown" class="me-1">Espectacle</label>
                                <select id="p3limitdropdown" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="60">Últims 60</option>
                                    <option notransval="1" value="120">Últims 120</option>
                                    <option notransval="1" value="250">Darrers 250</option>
                                    <option notransval="1" value="500">Últims 500</option>
                                    <option notransval="1" value="1000">Últims 1000</option>
                                    <option notransval="1" value="">Sense límit</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(2)"><i class="fa-solid fa-download" title="Descarrega Esdeveniments"></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">Els meus usuaris</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 tot">
                                <input type="button" id="UsersGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Acció grupal" onclick="p3usersGroupActionFunction()">
                                <input id="UserNewAccountButton" class="btn btn-success btn-sm me-1" type="button" onclick="showCreateNewAccountDialog()" value="Compte nou...">
                                <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()">Transmissió</button>
                                        <button type="button" class="dropdown-item" onclick="p4downloadUserInfo()">Descarrega la informació de l'usuari</button>
                                        <button type="button" id="p4MobileUserBatchCreate" class="dropdown-item" style="display:none" onclick="p4batchAccountCreate()">Crear comptes per lots</button>
                                        <button type="button" class="dropdown-item" onclick="onUsersViewSettings()">Configuració</button>
                                    </div>
                                </div>
                                <div class="d-inline-flex align-items-center">
                                    <input id="UserSearchInput" type="search" class="form-control-sm me-1" placeholder="Filtre" 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="Transmissió">
                                <a href="#" onclick="p4downloadUserInfo()"><i role="button" class="fa-solid fa-download me-2" title="Descarrega la informació de l'usuari"></i></a>
                                <a href="#" onclick="p4batchAccountCreate()"><i role="button" id="p4UserBatchCreate" style="display:none" title="Creeu per lots molts comptes d'usuari" 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="Configuració" 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">Els meus fitxers</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="Amunt">
                                <input type="button" id="p5SelectAllButton" class="btn btn-secondary btn-sm me-1" disabled="disabled" onclick="p5selectallfile();" value="Seleccionar tot">
                                <input type="button" id="p5RenameFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Canvia el nom" onclick="p5renamefile();">
                                <input type="button" id="p5DeleteFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Suprimeix" onclick="p5deletefile();">
                                <input type="button" id="p5ViewFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Edita" onclick="p5viewfile()">
                                <input type="button" id="p5NewFolderButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Carpeta nova" onclick="p5createfolder();">
                                <input type="button" id="p5UploadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Carrega" onclick="p5uploadFile()">
                                <input type="button" id="p5DownloadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="descarregar" onclick="p5downloadButton()">
                                <input type="button" id="p5CutButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Tallar" onclick="p5copyFile(1)">
                                <input type="button" id="p5CopyButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Còpia" onclick="p5copyFile(0)">
                                <input type="button" id="p5PasteButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Enganxa" 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()">Carpeta nova</button>
                                        <button type="button" disabled="disabled" id="p5MobileUploadButton" class="dropdown-item" onclick="p5uploadFile()">Carrega</button>
                                        <button type="button" disabled="disabled" id="p5MobileDownloadButton" class="dropdown-item" onclick="p5downloadButton()">descarregar</button>
                                        <button type="button" disabled="disabled" id="p5MobileCutButton" class="dropdown-item" onclick="p5copyFile(1)">Tallar</button>
                                        <button type="button" disabled="disabled" id="p5MobileCopyButton" class="dropdown-item" onclick="p5copyFile(0)">Còpia</button>
                                        <button type="button" disabled="disabled" id="p5MobilePasteButton" class="dropdown-item" onclick="p5pasteFile()">Enganxa</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">Ordena per nom</option>
                                    <option value="2">Ordena per mida</option>
                                    <option value="3">Ordena per data</option>
                                    <option value="4">Baixar pel nom</option>
                                    <option value="5">Baixar per mida</option>
                                    <option value="6">Descens per data</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>Aquests fitxers es comparteixen públicament, feu clic a "enllaç" per obtenir l'URL públic.</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">El meu servidor</div>
                        <img id="MainMeshImage" src="serverpic.ashx" class="ms-auto">
                    </div>
                    <div id="p2ServerActions">
                        <p><strong>Accions 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()">Descarrega la còpia de seguretat del servidor</a>
                            </div>
                            <div id="p2ServerActionsRestore">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showRestoreDlg()">Restaura el servidor amb una còpia de seguretat</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()">Còpia de seguretat de 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()">Comproveu la versió del servidor</a>
                            </div>
                            <div id="p2ServerActionsErrors">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showErrorsDlg()">Mostra el registre d'errors del servidor</a>
                            </div>
                            <div id="p2ServerActionsConfig">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showConfigDlg()">Mostra la configuració del servidor</a>
                            </div>
                        </div>
                        <br>
                    </div>
                    <strong>Estadístiques 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>Avisos 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="esquena" 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>Notes</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="Destacat" 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="Llista de sessions remotes actives en aquest ordinador." 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="Ajuda sol·licitada" 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="esquena" 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>Escriptori<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">NUM</div>
                        <div id="p11capslock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">CAPS</div>
                        <div id="p11scrolllock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">ROLL</div>
                    </div>
                    <div id="devListToolbarViewIcons">
                        <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick="deskToggleFull(event)" title="Pantalla completa. Manteniu premuda la majúscula a pantalla completa del navegador."></i>
                    </div>
                </div>
                <div id="p11warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">El port de redirecció o la funció KVM està desactivada<span id="p11warninga">, feu clic aquí per activar-lo.</span></div>
                </div>
                <div id="p11warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">L'ordinador remot no està encès, feu clic aquí per emetre una ordre d'engegada.
                    </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="Connexió automàtica" 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="Connecteu-vos mitjançant l'escriptori remot MeshAgent" onclick="connectDesktop(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Connecta't</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)">Demanar consentiment + barra</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(2,event)">Demanar consentiment</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(3,event)">Barra de privadesa</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="Connecteu-vos mitjançant RDP" onclick="askRdpCredentials()" onkeypress="return false" onkeydown="return false" disabled="disabled">RDP Connect</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)">Port alternatiu</a></li>
                                </ul>
                            </div>
                            <span id="connectbutton1hspan"><button class="btn btn-primary btn-sm me-1" type="button" id="connectbutton1h" title="Connecteu-vos mitjançant KVM de maquinari" onclick="connectDesktop(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">HW Connect</button></span>
                            <div class="btn-group dropdown-center me-1" id="disconnectbutton1span">
                                <button type="button" class="btn btn-danger btn-sm" id="disconnectbutton1" title="Connecteu-vos mitjançant l'escriptori remot MeshAgent" onclick="connectDesktop(event,0)" onkeypress="return false" onkeydown="return false">Desconnecta</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)">Desconnectar i bloquejar</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(11,event)">Desconnecta</a></li>
                                </ul>
                            </div>
                            <span id="deskstatus" style="line-height:22px">Desconnectat</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="Canvieu l'estat d'alimentació de la màquina remota" onkeypress="return false" onkeydown="return false" value="Accions de poder..." onclick="showPowerActionDlg()" style="display:none">
                            <input id="deskActionsBtn" type="button" class="btn btn-primary btn-sm me-1" title="Realitzeu accions d'alimentació al dispositiu" onkeypress="return false" onkeydown="return false" value="Accions" onclick="deviceActionFunction()">
                            <input id="deskActionsSettings" type="button" class="btn btn-primary btn-sm me-1" value="Configuració" title="Editeu la configuració de l'escriptori remot" onkeypress="return false" onkeydown="return false" onclick="showDesktopSettings()">
                            <input id="deskFocusBtn" type="button" title="Canvia el mode d'enfocament, quan està actiu només s'actualitza la regió al voltant del ratolí" onkeypress="return false" onkeydown="return false" value="Enfoca-ho tot" onclick="deskToggleFocus()" style="margin-right:3px;display:none">
                            <div id="deskRecordIcon" class="deskareaicon" title="El servidor està gravant aquesta sessió" style="display:none;background-color:red;width:12px;height:12px;border-radius:6px;margin-top:5px"></div>
                            <div class="deskareaicon" title="Gira a la dreta" onclick="drotate(1)"><i class="fa-solid fa-rotate-right"></i></div>
                            <div class="deskareaicon" title="Gira a l'esquerra" onclick="drotate(-1)"><i class="fa-solid fa-rotate-left"></i></div>
                            <div class="deskareaicon" title="Canvia el mode de visualització" 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()">Actualització</a>
                                <div id="deskToolsTopTabProcess" class="deskToolsTopTab" onclick="changeDeskToolTab(0)" style="left:0px;bottom:0px">Processos</div>
                                <div id="deskToolsTopTabService" class="deskToolsTopTab" onclick="changeDeskToolTab(1)" style="display:none;left:90px;color:gray">Serveis</div>
                            </div>
                            <div id="deskToolsArea">
                                <div id="DeskToolsProcessTab">
                                    <div id="deskToolsHeader">
                                        <a class="colmn1" title="Ordena per ID de procés" onclick="sortProcess(0)">PID</a>
                                        <a class="colmn2" title="Ordena per nom" onclick="sortProcess(1)">Nom</a>
                                    </div>
                                    <div id="DeskToolsProcesses" style=""></div>
                                </div>
                                <div id="DeskToolsServiceTab" style="display:none">
                                    <div id="deskToolsServiceHeader">
                                        <a class="colmn1" style="width:70px" title="Ordena per estat" onclick="sortService(0)">Estat</a>
                                        <a class="colmn2" title="Ordena per nom" onclick="sortService(1)">Nom</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="Porta-retalls" onkeypress="return false" onkeydown="return false" onclick="showDeskClip()">
                            <input id="DeskType" class="btn btn-secondary btn-sm me-1" cmenu="deskPreConfigShortcutContextMenu" type="button" value="Tipus" 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="Mostra/amaga el teclat en pantalla" style="display:none">
                                <i class="fa-solid fa-keyboard"></i>
                            </button>
                            <div id="DeskControlSpan" title="Commuta l'entrada del ratolí i el teclat" 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">Entrada</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 accions">
                                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="Latència de sessió d'escriptori"></span>
                            <span id="DeskTimer" style="width:70px" class="text-center" title="Temps de sessió"></span>
                            <input id="DeskToolsButton" type="button" class="btn btn-primary btn-sm mx-1" value="Eines" title="Commuta la vista d'eines" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()">
                            <span id="DeskGuestShareButton" title="Comparteix el dispositiu amb un convidat" role="button">
                                <i class="fa-solid fa-fw fa-share-nodes" id="DeskInputUnLockedButtonImage" onclick="showShareDevice()"></i>
                            </span>
                            <span id="DeskInputUnLockedButton" title="L'entrada remota està desbloquejada" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" onclick="deskInputLockFunction(1)"></i>
                            </span>
                            <span id="DeskInputLockedButton" title="L'entrada remota està bloquejada" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" style="color:red" onclick="deskInputLockFunction(0)"></i>
                            </span>
                            <span id="DeskRefreshButton" title="Actualitzeu l'escriptori" role="button">
                                <i class="fa-solid fa-fw fa-rotate" onclick="deskRefreshFunction()"></i>
                            </span>
                            <span id="DeskClipboardOutButton" title="Carregueu el porta-retalls local al dispositiu remot" 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="Baixeu el porta-retalls remot al porta-retalls 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="Grava la sessió d'escriptori remot a un fitxer" style="display:none" role="button">
                                <i class="fa-solid fa-fw fa-video" onclick="deskRecordSession()" id="DeskRecordButtonImage"></i>
                            </span>
                            <span id="DeskSaveImageButton" title="Deseu una captura de pantalla de l'escriptori remot" role="button">
                                <i class="fa-solid fa-fw fa-camera" onclick="deskSaveImage()"></i>
                            </span>
                            <span id="DeskBackgroundButton" title="Canvia el fons de l'escriptori remot" role="button">
                                <i class="fa-solid fa-fw fa-image" onclick="deviceToggleBackground(event)" id="DeskBackgroundButtonImage"></i>
                            </span>
                            <span id="DeskOpenWebButton" title="Obriu una adreça web a l'ordinador remot" role="button">
                                <i class="fa-solid fa-fw fa-globe" onclick="deviceUrlFunction()"></i>
                            </span>
                            <span id="DeskLockButton" title="Bloqueja l'ordinador remot" role="button">
                                <i class="fa-solid fa-fw fa-lock" onclick="deviceLockFunction()"></i>
                            </span>
                            <span id="DeskNotifyButton" title="Mostra una notificació a l'ordinador remot" role="button">
                                <i class="fa-solid fa-fw fa-bell" onclick="deviceToastFunction()"></i>
                            </span>
                            <span id="DeskChatButton" class="deskarea" title="Obre la finestra de xat en aquest ordinador" role="button">
                                <i class="fa-solid fa-fw fa-message" onclick="deviceChat(event)"></i>
                            </span>
                            <span id="DeskRunButton" cmenu="deskPreConfigScriptContextMenu" class="deskarea" title="Executeu un script en aquest ordinador" 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>Selecciona el 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>Teclat 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>Escriu text</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>Accions</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>Configuració</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>Activa el mode de trackpad</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>Activa el mode de clic dret</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>Personalitza les dreceres</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>Actualitza l'escriptori</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>Porta-retalls</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>Carrega el porta-retalls</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>Obre l'URL al remot</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>Bloqueig remot</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>Envia una notificació</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>Xatejar</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>Executa l'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="esquena" 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. Manteniu premuda la majúscula a pantalla completa del navegador."></i>
                    </div>
                </div>
                <div id="p12warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">El port de redirecció o la funció KVM està desactivada<span id="p12warninga">, feu clic aquí per activar-lo.</span></div>
                </div>
                <div id="p12warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">L'ordinador remot no està encès, feu clic aquí per emetre una ordre d'engegada.
                </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>
                                        Connexió automàtica</button>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2span">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2" title="Connecteu-vos mitjançant l'escriptori remot MeshAgent" onclick="connectTerminal(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Connecta't</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>Shell d'administració</b></a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0,event)">Administrador PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Shell d'usuari</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Usuari PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0x10,event)">Pregunteu a l'administrador Shell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0x10,event)">Pregunteu a l'administrador PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0x10,event)">Pregunteu a User Shell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0x10,event)">Pregunteu a l'usuari PowerShell</a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(8,0,event)"><b>Shell d'usuari</b></a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Usuari PowerShell</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Shell d'arrel</b></a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Shell d'usuari</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(100,0,event)">Inici de sessió Shell</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="Connecteu-vos mitjançant l'escriptori remot MeshAgent" onclick="connectTerminal(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Connexió 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)">Port alternatiu</a></li>
                                        </ul>
                                    </div>
                                    <span id="connectbutton2hspan"><button type="button" class="btn btn-success btn-sm me-1" id="connectbutton2h" title="Connecteu-vos mitjançant KVM de maquinari Intel® AMT" onclick="connectTerminal(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">HW Connect</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">Desconnecta</button></span>
                                    <span id="termstatus" style="line-height:22px">Desconnectat</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à gravant aquesta sessió" 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="Realitzeu accions d'alimentació al dispositiu" onkeypress="return false" onkeydown="return false" value="Accions" 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="Retrocés" 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="Enganxa" title="Enganxeu text al terminal" onclick="showTermPasteDialog()" style="display:none">
                                </div>
                                <div class="d-flex align-items-center ms-auto">
                                    <span id="TermTimer" title="Temps de sessió"></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="Commuta el que enviarà la clau de retorn" 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="Canvia el tipus d'emulació de tecles 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 estès" title="Canvia el tipus d'emulació 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">100 x 30</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="Envia la clau 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="esquena" 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>Fitxers<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> Connexió automàtica</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connect" title="Connecteu-vos mitjançant l'escriptori remot MeshAgent" onclick="connectFiles(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Connecta't</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()">Demanar consentiment</a></li>
                                    </ul>
                                </div>
                                <button id="p13Disconnect" class="btn btn-danger btn-sm me-1" onclick="connectFiles(event)" type="button">Desconnecta</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectsspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connects" title="Connecteu-vos mitjançant l'escriptori remot MeshAgent" onclick="connectFiles(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">Connexió 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)">Port alternatiu</a></li>
                                    </ul>
                                </div>
                                <span id="p13Status">Desconnectat</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="Realitzeu accions d'alimentació al dispositiu" value="Accions" onclick="deviceActionFunction()">
                                <div id="filesRecordIcon" class="deskareaicon" title="El servidor està gravant aquesta sessió" 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="Amunt" onclick="p13folderup()">
                                <input type="button" disabled="disabled" id="p13SelectAllButton" class="btn btn-secondary me-1 btn-sm" value="Seleccionar tot" onclick="p13selectallfile()">
                                <input type="button" disabled="disabled" id="p13RenameFileButton" class="btn btn-primary me-1 btn-sm" value="Canvia el nom" onclick="p13renamefile()">
                                <input type="button" disabled="disabled" id="p13DeleteFileButton" class="btn btn-primary me-1 btn-sm" value="Suprimeix" onclick="p13deletefile()">
                                <input type="button" disabled="disabled" id="p13ViewFileButton" class="btn btn-primary me-1 btn-sm" value="Edita" onclick="p13viewfile()">
                                <input type="button" disabled="disabled" id="p13NewFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Carpeta nova" onclick="p13createfolder()">
                                <input type="button" disabled="disabled" id="p13NewFileButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Fitxer nou" onclick="p13createfile()">
                                <input type="button" disabled="disabled" id="p13UploadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Carrega" onclick="p13uploadFile()">
                                <input type="button" disabled="disabled" id="p13DownloadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="descarregar" onclick="p13downloadButton()">
                                <input type="button" disabled="disabled" id="p13CutButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Tallar" onclick="p13copyFile(1)">
                                <input type="button" disabled="disabled" id="p13CopyButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Còpia" onclick="p13copyFile(0)">
                                <input type="button" disabled="disabled" id="p13PasteButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Enganxa" onclick="p13pasteFile()">
                                <input type="button" disabled="disabled" id="p13ZipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Zip" onclick="p13zipFiles()">
                                <input type="button" disabled="disabled" id="p13UnzipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Descomprimiu" onclick="p13unzipFile()">
                                <input type="button" disabled="disabled" id="p13RefreshButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Actualització" onclick="p13folderup(9999)">
                                <input type="button" disabled="disabled" id="p13FindButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Troba" onclick="p13findfile()">
                                <input type="button" disabled="disabled" id="p13GoToFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Anar a" onclick="p13gotofolder()">
                                <input type="button" disabled="disabled" id="p13OpenButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Obert" 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()">Carpeta nova</button>
                                        <button type="button" disabled="disabled" id="p13MobileNewFileButton" class="dropdown-item" onclick="p13createfile()">Fitxer nou</button>
                                        <button type="button" disabled="disabled" id="p13MobileUploadButton" class="dropdown-item" onclick="p13uploadFile()">Carrega</button>
                                        <button type="button" disabled="disabled" id="p13MobileDownloadButton" class="dropdown-item" onclick="p13downloadButton()">descarregar</button>
                                        <button type="button" disabled="disabled" id="p13MobileCutButton" class="dropdown-item" onclick="p13copyFile(1)">Tallar</button>
                                        <button type="button" disabled="disabled" id="p13MobileCopyButton" class="dropdown-item" onclick="p13copyFile(0)">Còpia</button>
                                        <button type="button" disabled="disabled" id="p13MobilePasteButton" class="dropdown-item" onclick="p13pasteFile()">Enganxa</button>
                                        <button type="button" disabled="disabled" id="p13MobileZipButton" class="dropdown-item" onclick="p13zipFiles()">Zip</button>
                                        <button type="button" disabled="disabled" id="p13MobileUnzipButton" class="dropdown-item" onclick="p13unzipFile()">Descomprimiu</button>
                                        <button type="button" disabled="disabled" id="p13MobileRefreshButton" class="dropdown-item" onclick="p13folderup(9999)">Actualització</button>
                                        <button type="button" disabled="disabled" id="p13MobileFindButton" class="dropdown-item" onclick="p13findfile()">Troba</button>
                                        <button type="button" disabled="disabled" id="p13MobileGoToFolderButton" class="dropdown-item" onclick="p13gotofolder()">Anar a</button>
                                        <button type="button" disabled="disabled" id="p13MobileOpenButton" class="dropdown-item" onclick="p13openfilefolder()">Obert</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">Llegible per a persones</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">Ordena per nom</option>
                                    <option value="2">Ordena per mida</option>
                                    <option value="3">Ordena per data</option>
                                    <option value="4">Baixar pel nom</option>
                                    <option value="5">Baixar per mida</option>
                                    <option value="6">Descens per data</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="esquena" 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. Manteniu premuda la majúscula a pantalla completa del navegador."></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="esquena" 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ó sobre el nucli actual que s'executa en aquest agent"></div>
                                <input type="button" id="p15uploadCore" class="btn btn-primary me-1 btn-sm" value="Acció de l'agent" onclick="p15uploadCore(event)" title="Canvieu el mòdul de codi Java Script de l'agent">
                                <i onclick="p15downloadConsoleText()" title="Descarrega el text de la 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">Agent</option>
                                            <option id="p15outputselect3" value="3">Empènyer</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="Clar" 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="esquena" 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>Esdeveniments<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">Filtre</label>
                                <select id="p16filterevents" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="">Tots els registres</option>
                                    <option notransval="1" value="agentlog">Registres d'agents</option>
                                    <option notransval="1" value="relaylog">Registres de relleus</option>
                                    <option notransval="1" value="manual">Registres manuals</option>
                                    <option notransval="1" value="runcommands">Executeu registres d'ordres</option>
                                    <option notransval="1" value="batchupload">Registres de càrrega per lots</option>
                                    <option notransval="1" value="changenode">Canvia els registres de nodes</option>
                                    <option notransval="1" value="removenode">Elimina els registres de nodes</option>
                                </select>
                                <label class="me-1" for="p16limitdropdown">Espectacle</label>
                                <select id="p16limitdropdown" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="60">Últims 60</option>
                                    <option notransval="1" value="120">Últims 120</option>
                                    <option notransval="1" value="250">Darrers 250</option>
                                    <option notransval="1" value="500">Últims 500</option>
                                    <option notransval="1" value="1000">Últims 1000</option>
                                </select>
                                <i onclick="p3showDownloadEventsDialog(1)" class="fa-solid fa-download" title="Descarrega Esdeveniments" 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="esquena" 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>Detalls<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="Actualitzeu la informació detallada."></i>
                        <i class="fa-solid fa-chart-line fa-xl fa-border" role="button" onclick="deskToggleCpuGraph(event)" title="Mostra l'ús de la memòria i la CPU del dispositiu."></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àfic en viu</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="esquena" 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>Programari<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="Actualització" onclick="loadInstalledApps()">
                        <input type="text" id="p18SearchInput" class="form-control-sm" placeholder="Cerca programari..." style="width:250px" onkeyup="filterInstalledApps()">
                        <label style="margin-left:15px">
                            <input type="checkbox" id="p18ShowStore" onchange="toggleStoreApps()"> Mostra les aplicacions de la botiga
                        </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>Feu clic a Actualitza per carregar el programari instal·lat...</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="esquena" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Connectors<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="esquena" 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="esquena" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Resum - <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">Estats de poder</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">Tipus d'agents</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">Connectivitat</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">Seguretat</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="esquena" 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="esquena" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Esdeveniments<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">Filtre</label>
                                <select id="p31filterevents" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="">Tots els registres</option>
                                    <option notransval="1" value="agentlog">Registres d'agents</option>
                                    <option notransval="1" value="relaylog">Registres de relleus</option>
                                    <option notransval="1" value="manual">Registres manuals</option>
                                    <option notransval="1" value="runcommands">Executeu registres d'ordres</option>
                                    <option notransval="1" value="batchupload">Registres de càrrega per lots</option>
                                    <option notransval="1" value="changenode">Canvia els registres de nodes</option>
                                    <option notransval="1" value="removenode">Elimina els registres de nodes</option>
                                </select>
                                <label for="p31limitdropdown" class="me-1">Espectacle</label>
                                <select id="p31limitdropdown" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="60">Últims 60</option>
                                    <option notransval="1" value="120">Últims 120</option>
                                    <option notransval="1" value="250">Darrers 250</option>
                                    <option notransval="1" value="500">Últims 500</option>
                                    <option notransval="1" value="1000">Últims 1000</option>
                                    <option notransval="1" value="">Sense límit</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(3)"><i class="fa-solid fa-download" role="button" title="Descarrega Esdeveniments"></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">Les meves estadístiques 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="Actualització" 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">Registre-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">Connexions</option>
                            <option value="1">Memòria</option>
                            <option value="5">CPU</option>
                            <option value="3">Trànsit entrant</option>
                            <option value="4">Trànsit Sortint</option>
                        </select>
                        <select id="p40time" class="form-select-sm me-1" onchange="updateServerTimelineHours()">
                            <option value="3">Últimes 3 hores</option>
                            <option value="8">Últimes 8 hores</option>
                            <option value="24">L'últim dia</option>
                            <option value="168">La setmana passada</option>
                            <option value="720">Últims 30 dies</option>
                        </select>
                        <i class="fa-solid fa-download" title="Baixa punts de dades (.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">Seguiment del meu servidor</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Traçat" type="button" class="btn btn-primary btn-sm me-1" onclick="setServerTracing()">
                        <span id="p41traceStatus">Cap</span>
                    </div>
                    <div class="toright2 d-flex align-items-center ms-auto">
                        <label for="p41limitdropdown" class="me-1">Espectacle</label>
                        <select id="p41limitdropdown" class="form-select-sm me-1" onchange="displayServerTrace()">
                            <option value="100">Últims 100</option>
                            <option value="250">Darrers 250</option>
                            <option value="500">Últims 500</option>
                            <option value="1000">Últims 1000</option>
                        </select>
                        <input value="Clar" type="button" class="btn btn-primary btn-sm me-1" onclick="clearServerTracing()">
                        <i class="fa-solid fa-download" title="Baixa traça (.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">Els meus connectors de servidor</div>
                </div>
                <div class="areaHead">
                    <div class="toright2">
                    </div>
                    <div>
                        <input value="Descarrega el connector" type="button" onclick="return pluginHandler.addPluginDlg();">
                    </div>
                </div>
                <div id="pluginRestartNotice" class="areaHead" style="background-color:gold;display:none">
                    <div class="toright2">
                        <input value="Actualitzar els nuclis de l'agent" type="button" onclick="distributeCore();return false">
                    </div>
                    <div style="padding:2px">
                        <div style="padding:2px"><b>Avís:</b> Els connectors s'han modificat, això pot requerir una actualització del nucli de l'agent.</div>
                    </div>
                </div>
                <table id="p42tbl">
                    <tbody><tr class="DevSt">
                        <th style="width:26px"></th>
                        <th style="width:10px"></th>
                        <th class="chName">Nom</th>
                        <th class="chDescription">Descripció</th>
                        <th class="chSite" style="text-align:center">Enllaç</th>
                        <th class="chVersion" style="text-align:center">Versió</th>
                        <th class="chUpgradeAvail" style="text-align:center">Última</th>
                        <th class="chStatus" style="text-align:center">Estat</th>
                        <th class="chAction" style="text-align:center">Acció</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 hi ha connectors al servidor.</i></div>
            </div>
            <div id="p43" style="display:none">
                <div id="p43BackButton">
                    <div class="backButton" tabindex="0" onmouseup="go(42)" title="esquena" onkeypress="if (event.key == 'Enter') go(42)">
                        <div class="backButtonEx"></div>
                    </div>
                </div>
                <h1>Els meus connectors 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">Els meus grups d'usuaris</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 tot">
                                <input type="button" id="UsersGroupsGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Acció grupal" onclick="p50usersGroupActionFunction()">
                                <input id="NewUserGroupButton" type="button" class="btn btn-success btn-sm me-1" onclick="showCreateUserGroupDialog(1)" value="Nou grup...">
                                <input id="DuplicateUserGroupButton" type="button" class="btn btn-primary button-outline-success btn-sm me-1" style="display:none" onclick="showCreateUserGroupDialog(2)" value="Grup duplicat...">
                            </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="esquena" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Grup d'usuaris - <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">Les meves gravacions d'usuari</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="Actualització">
                            </div>
                            <div class="d-flex align-items-center ms-auto">
                                <input type="button" class="btn btn-primary btn-sm" onclick="openRecodringPlayer()" value="Obre el 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">Els meus 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="Descarrega l'informe"></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> Genera un 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 Correu</a>
                        &nbsp;<a id="termsLinkFooter" href="terms">Condicions i privadesa</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ó de fitxers</div>
                                <select id="d3uploadMode" onchange="d3modechange()">
                                    <option value="1">Càrrega de fitxers locals</option>
                                    <option value="2">Selecció de fitxers del servidor</option>
                                </select>
                            </div>
                            <div id="d3localmode" style="display:none">
                                <div>Penja el document</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="Amunt">&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="Embolcalla" onclick="d4ToggleWrap()">
                            <input id="d4SizeButton" class="btn btn-primary me-1 mb-1" type="button" value="Petit" onclick="d4ToggleSize()">
                            <input id="d4EncodingButton" class="btn btn-primary me-1 mb-1" type="button" value="En cru" 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')">Agent</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">Qualitat</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">Escalat</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7framelimiter" class="form-select">
                                        <option selected="selected" value="50">Ràpid</option>
                                        <option value="100">Mitjana</option>
                                        <option value="400">Lent</option>
                                        <option value="1000">Molt lent</option>
                                    </select>
                                    <label for="d7framelimiter">Velocitat de fotogrames</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">WEB</option>
                                    </select>
                                    <label for="d7encoding">Codificació</label>
                                </div>
                                <fieldset id="d7desktopOtherSettings">
                                    <legend>Altres configuracions</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">Canvia els botons del ratolí</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskrmw" class="form-check-label">Roda inversa del ratolí</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskRemoteKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskRemoteKeyMap" class="form-check-label">Utilitzeu el mapa de teclat remot</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">Porta-retalls automàtic</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">Bloqueja a Desconnecta</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, el més ràpid</option>
                                        <option value="2">RLE16, recomanat</option>
                                        <option value="3">RAW8, Lent</option>
                                        <option value="4">RAW16, molt lent</option>
                                    </select>
                                    <label for="d7desktopmode">Codificació d'imatges</label>
                                </div>
                                <fieldset>
                                    <legend>Altres configuracions</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">Mostra l'eina de focus</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7showcursor" class="form-check-input me-2" type="checkbox">
                                            <label for="d7showcursor" class="form-check-label">Mostra el cursor del ratolí 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 local del teclat</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7kvmrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7kvmrmw" class="form-check-label">Roda inversa del ratolí</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">Mida del llenç</option>
                                        <option notransval="1" value="browser">Mida del navegador</option>
                                        <option notransval="1" value="screen">Mida de la 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">Mida de la pantalla</label>
                                </div>
                                <fieldset>
                                    <legend>Opcions</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">Desactiva el fons 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">Desactiva l'arrossegament de la finestra 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">Desactiva les animacions del 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">Desactiva la temàtica</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp6" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp6" class="form-check-label">Desactiva l'ombra 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">Desactiva la configuració 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">Habiliteu la suavització de fonts</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp9" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp9" class="form-check-label">Activa la composició de l'escriptori</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpclip" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpclip" class="form-check-label">Porta-retalls automàtic</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpsmb" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpsmb" class="form-check-label">Canvia els botons del ratolí</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdprmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdprmw" class="form-check-label">Roda inversa del ratolí</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">Cancel · lar</button>
                        <button type="button" class="btn btn-primary" id="idx_dlgOkButton">D'acord</button>
                        <button type="button" class="btn btn-danger" id="idx_dlgDeleteButton" style="display:none">Suprimeix</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 = ['', "Alimentat", "Dormir", "Dormir", "Dormir", "Hibernant", "Apagar", "Present"];
        var StatusStrs = ["Desconnectat", "Connectant...", "Configuració...", "Connectat", "Intel&reg; AMT connectat"];
        var agentsStr = ["Desconegut", "Consola Windows de 32 bits", "Consola Windows de 64 bits", "Servei Windows de 32 bits", "Servei Windows de 64 bits", "Linux de 32 bits", "Linux de 64 bits", "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 Windows MinCore", "Servei Windows MinCore", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 de 64 bits", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Apple Silicon", "FreeBSD x86-64", "Desconegut", "Linux ARM de 64 bits", "Alpine Linux x86 de 64 bits (MUSL)", "Assistent (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Desconegut", "Desconegut", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Consola Windows ARM de 64 bits", "Servei Windows ARM de 64 bits", "ARMVIRT32 (OpenWRT)", "RISC-V 64bit"];
        var agentsStrNoAgent = ['', '', '', '', "Windows", '', "Linux", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', "poma"];
        var domainStates = ["Cap", "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]|Tipus");

            // 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>' + "Navegador predeterminat" + '</label><br>';
            x += '<input type=radio id=night2 name=nightmoderadio value=2 ' + ((cNightMode == 2) ? 'checked' : '') + '><label for=night2>' + "Mode de llum" + '</label><br>';
            x += '<input type=radio id=night1 name=nightmoderadio value=1 ' + ((cNightMode == 1) ? 'checked' : '') + '><label for=night1>' + "Mode fosc" + '</label><br>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Mode nocturn", 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', "Interfície d'usuari", 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 es pot realitzar l'autenticació"); return; }
                if (errorCode == 'invalidorigin') { QH('p0span', "Origen no vàlid a la sol·licitud HTTP"); return; }
                if (prevState == 2) { if (autoReconnect) { setTimeout(serverPoll, 5000); } } else { QH('p0span', "No es pot connectar el sòcol web"); }
                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: "Afegiu codis de seguretat de dos factors. Si es perd el factor actual, no hi ha manera de recuperar aquest compte.", title: "Autenticació de dos factors", 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', " - Restableix el següent inici de sessió."); }
                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', " - Restableix el següent inici de sessió."); }
                    else if (seconds < 3600) { var secs = Math.floor(seconds / 60); QH('p2nextPasswordUpdateTime', format((secs == 1) ? " - Restablir en 1 minut." : " - Restableix d'aquí a {0} minuts.", secs)); }
                    else if (seconds < 86400) { var hours = Math.floor(seconds / 3600); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Restablir en 1 hora." : " - Restableix d'aquí a {0} hores.", hours)); }
                    else { var days = Math.floor(seconds / 86400); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Restablir en 1 dia." : " - Restableix d'aquí a {0} dies.", 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 second until logout" : "{0} seconds until logout")
                        : (ds == 1 ? "1 segon fins desconnectar" : "{0} segons per desconnectar");
                } else {
                    ds = Math.round(ds / 60);
                    if (ds <= 5) {
                        theText = isLogout
                            ? (ds == 1 ? "1 minute until logout" : "{0} minutes until logout")
                            : (ds == 1 ? "1 minut per desconnectar" : "{0} minuts per desconnectar");
                    }
                }
                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', "Cap");
                        }
                    }
                    break;
                }
                case 'serverstats': {
                    updateGeneralServerStats(message);
                    break;
                }
                case 'serverwarnings': {
                    if ((message.warnings != null) && (message.warnings.length > 0)) {
                        var ServerWarnings = {
                            1: "",
                            2: "Falten paràmetres WebDAV.",
                            3: "Opció de configuració no reconeguda \"{0}\".",
                            4: "La compressió de WebSocket està desactivada, aquesta característica està trencada a NodeJS v11.11 a v12.15 i v13.2",
                            5: "No es pot carregar el certificat arrel Intel AMT TLS per al domini predeterminat.",
                            6: "No es pot carregar el certificat arrel Intel AMT TLS per al domini {0}.",
                            7: "Els FQDN locals de CIRA s'ignoren quan el servidor està en mode només LAN o només WAN.",
                            8: "No pot tenir més de 4 FQDN locals CIRA. Ignorant el valor.",
                            9: "La comprovació hash de l'agent s'està omet, això no és segur.",
                            10: "Falta l'adreça de correu electrònic de Let's Encrypt.",
                            11: "Els noms d'amfitrió de Let's Encrypt no són vàlids.",
                            12: "Els noms de Let's Encrypt no són vàlids, no poden contenir un *.",
                            13: "No es pot configurar el mòdul Let's Encrypt.",
                            14: "Noms Let's Encrypt no vàlids, no es poden resoldre: {0}",
                            15: "L'adreça electrònica de Let's Encrypt no és vàlida, no es pot resoldre: {0}",
                            16: "No es pot carregar la llista d'adreces IPv6 del servidor intermediari de confiança de CloudFlare.",
                            17: "El servidor SendGrid té un ús limitat en mode LAN.",
                            18: "El servidor SMTP té un ús limitat en mode LAN.",
                            19: "La passarel·la SMS té un ús limitat en mode LAN.",
                            20: "\"LoginCookieEncryptionKey\" no vàlid a config.json.",
                            21: "El camí de còpia de seguretat no es pot establir dins de la carpeta de dades meshcentral, la configuració de còpia de seguretat ignorada.",
                            22: "No s'ha pogut signar l'agent {0}: {1}",
                            23: "No es pot carregar el fitxer d'icona de l'agent: {0}.",
                            24: "No es pot carregar el fitxer del logotip de l'agent: {0}.",
                            25: "Aquesta versió de NodeJS no admet OpenID.",
                            26: "Aquesta versió de NodeJS no és compatible amb 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>' + "ADVERTIMENT:" + 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>' + "ADVERTIMENT:" + 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>' + "ADVERTIMENT:" + format("El certificat caduca d'aquí a {0} dies", days) + '</b></div>');
                            QV('serverWarningsDiv', true);
                            addNotification({ text: format("El certificat caduca d'aquí a {0} dies", 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>' + "Feu clic a Actualitza per carregar el programari instal·lat..." + '</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>' + "Vist per últim cop:" + '<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>' + "Èxit" + '</span>' : '<span style=color:red>' + "Fracassat" + '</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>' + "Nom del procés" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.processName + '</b></div></div>'; }
                                        if (message.value.machineName) { x += addHtmlValue5("Nom de la màquina", message.value.machineName); }
                                        if (message.value.cmd) { x += '<div style=padding:4px><div style=padding-bottom:4px>' + "Línia d'ordres" + '</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ítol de la finestra" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.mainWindowTitle + '</b></div></div>'; }
                                        if (message.value.processUser) { x += addHtmlValue5("Usuari", message.value.processUser); }
                                        if (message.value.processDomain) { x += addHtmlValue5("Domini", message.value.processDomain); }
                                        if (message.value.startTime) { x += addHtmlValue5("L'hora d'inici", message.value.startTime); }
                                        x += '<hr />';
                                        if (message.value.PriorityBoostEnabled) { x += addHtmlValue5("Impuls de prioritat", message.value.PriorityBoostEnabled ? "Habilitat" : "Inhabilitat"); }
                                        if (message.value.sessionId) { x += addHtmlValue5("ID de sessió", message.value.sessionId); }
                                        if (message.value.handleCount) { x += addHtmlValue5("Recompte de maneig", message.value.handleCount); }
                                        if (message.value.privilegedProcessorTime) { x += addHtmlValue5("Temps de processador privilegiat", format("{0} segons", message.value.privilegedProcessorTime)); }
                                        if (message.value.totalProcessorTime) { x += addHtmlValue5("Temps total del processador", format("{0} segons", message.value.totalProcessorTime)); }
                                        if (message.value.userProcessorTime) { x += addHtmlValue5("Temps del processador de l'usuari", format("{0} segons", message.value.userProcessorTime)); }
                                        if (message.value.nonpagedSystemMemorySize) { x += addHtmlValue5("Memòria no paginada", getNiceSize2(message.value.nonpagedSystemMemorySize)); }
                                        if (message.value.pagedMemorySize) { x += addHtmlValue5("Memòria paginada", getNiceSize2(message.value.pagedMemorySize)); }
                                        if (message.value.privateMemorySize) { x += addHtmlValue5("Memòria privada", getNiceSize2(message.value.privateMemorySize)); }
                                        if (message.value.virtualMemorySize) { x += addHtmlValue5("Memòria virtual", getNiceSize2(message.value.virtualMemorySize)); }
                                        if (message.value.workingSet) { x += addHtmlValue5("Conjunt de treball", getNiceSize2(message.value.workingSet)); }
                                        if (message.value.peakWorkingSet) { x += addHtmlValue5("Conjunt de treball pic", getNiceSize2(message.value.peakWorkingSet)); }
                                        if (message.value.peakPagedMemorySize) { x += addHtmlValue5("Memòria de pàgina màxima", getNiceSize2(message.value.peakPagedMemorySize)); }
                                        if (message.value.peakVirtualMemorySize) { x += addHtmlValue5("Memòria virtual màxima", getNiceSize2(message.value.peakVirtualMemorySize)); }
                                    } else {
                                        x += "No s'ha proporcionat informació";
                                    }
                                    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("Primera connexió d'agent", printDateTime(new Date(currentNode.firstconnect))); }
                        if (currentNode.lastconnect) { x += addHtmlValue2("Última connexió d'agent", printDateTime(new Date(currentNode.lastconnect))); }
                        if (currentNode.lastaddr) {
                            var splitip = currentNode.lastaddr.split(':');
                            if (splitip.length > 2) {
                                // IPv6
                                x += addHtmlValue2("Adreça de l'últim agent", currentNode.lastaddr + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(currentNode.lastaddr) + '")></i>');
                            } else {
                                // IPv4
                                if (isPrivateIP(currentNode.lastaddr)) {
                                    x += addHtmlValue2("Adreça de l'últim agent", splitip[0] + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                } else {
                                    x += addHtmlValue2("Adreça de l'últim agent", '<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="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                }
                            }
                        }
                        if (message.updateTime != null) { x += addHtmlValue2("Última actualització de les interfícies", 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("Nom", '<b>' + EscapeHtml(net.name) + '</b>'); }
                                if (net.desc) { x += addHtmlValue2("Descripció", EscapeHtml(net.desc).replace('(R)', '&reg;').replace('(r)', '&reg;')); }
                                if (net.dnssuffix) { x += addHtmlValue2("Sufix DNS", EscapeHtml(net.dnssuffix) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia el nom al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.dnssuffix) + '")></i>'); }
                                if (net.mac) { x += addHtmlValue2("Adreça 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="' + "Copia l'adreça MAC al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.mac.toLowerCase()) + '")></i>'); }
                                if (net.v4addr) { x += addHtmlValue2("Adreça IPv4", EscapeHtml(net.v4addr) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" 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="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4mask) + '")></i>'); }
                                if (net.v4gateway) { x += addHtmlValue2("passarel·la IPv4", EscapeHtml(net.v4gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4gateway) + '")></i>'); }
                                if (net.gatewaymac) { x += addHtmlValue2("Gateway MAC", '<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="' + "Copia l'adreça MAC al porta-retalls" + '" 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("Nom", '<b>' + EscapeHtml(i) + '</b>');
                                if (typeof net[0].mac == 'string') { x += addHtmlValue2("Adreça 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="' + "Copia l'adreça MAC al porta-retalls" + '" 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("Adreça IPv6", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" 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="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("passarel·la IPv6", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    } else if (netif.family == 'IPv4') {
                                        if (netif.address) { x += addHtmlValue2("Adreça IPv4", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" 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="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("passarel·la IPv4", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Copia l'adreça al porta-retalls" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    }
                                }
                            }
                        } else {
                            x += '<hr />'
                            x += "No hi ha informació de la interfície de xarxa disponible per a aquest dispositiu.";
                        }
                        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 = "Desconegut"; }
                        if (!message.tags.stable) { message.tags.stable = "Desconegut"; }
                        if (!message.tags.latest) { message.tags.latest = "Desconegut"; }
                        x += addHtmlValue2("Versió 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 == "Desconegut") || (message.tags.current == message.tags.stable) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Versió 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 == "Desconegut") || (message.tags.current == message.tags.latest) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Última versió" + '</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ó MeshCentral", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        } else {
                            x += '<hr />' + "Marqueu i feu clic a D'acord per iniciar l'autoactualització del servidor.";
                            xxdialogTag = message.tags;
                            setModalContent('xxAddAgent', "Versió 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', "Errors del servidor", "El servidor no té registre d'errors.");
                            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="' + "Baixeu el registre d'errors" + '" onclick=d2CopyServerErrorsToClip()></i></div>';
                            x += '<div><label><input id=d2clearErrorsCheck type=checkbox class="form-check-input me-2" onclick=server_showErrorsDlgUpdate() /> ' + "Comproveu i feu clic a D'acord per esborrar el registre d'errors." + '</label></div>';
                            setModalContent('xxAddAgent', "Errors 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', "Configuració del servidor", 'Server has no config file.', 'extra-large');
                        } else {
                            setModalContent('xxAddAgent', "Configuració del servidor", 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à bloquejat", "Els codis de seguretat estan bloquejats", "Token d'inici de sessió en ús", "OTP 2FA no permesa", "El compte està bloquejat", "No es pot carregar OTPLIB"];
                            if ((message.err > 0) && (message.err < otpauthErrors.length)) { QH('d2optinfo', otpauthErrors[message.err]); } else { QH('d2optinfo', format("Error núm. {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("Instal·lar" + ' <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ó compatible i escanejar el codi de barres, utilitzeu <a href=\"{0}\" rel=\"noreferrer noopener\" target=_blank>aquest enllaç</a> o introduïu el secret. A continuació, introduïu el testimoni actual de 6 dígits a continuació per activar l'inici de sessió en dos passos.", message.url) + '<br /><br />' + 'Secret <img src=images/link4.png height=10 width=10 title="' + "Copia Secret al porta-retalls" + '" 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 />' + "Introduïu el testimoni aquí per iniciar sessió en dos passos:" + ' <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ó d'autenticació", message.success ? ('<b style=color:green>' + "S'ha activat correctament l'aplicació Authenticator." + '</b> ' + "Ara necessitareu un testimoni vàlid per tornar a iniciar sessió.") : ('<b style=color:red>' + "L'activació de l'inici de sessió en dos passos ha fallat." + '</b> ' + "Esborra el secret de l'aplicació i torna-ho a provar. Només teniu uns minuts per introduir el codi adequat."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-clear': {
                    if (xxdialogMode) return;
                    setModalContent('xxAddAgent', "Aplicació d'autenticació", message.success ? ('<b>' + "S'ha eliminat l'aplicació de l'autenticador." + '</b> ' + "Podeu reactivar aquesta funció en qualsevol moment.") : ('<b style=color:red>' + "L'eliminació de l'activació de l'inici de sessió en dos passos ha fallat." + '</b> ' + "Torna-ho a provar."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-getpasswords': {
                    if (xxdialogMode == 2 && (xxdialogTag != 'otpauth-manage')) return;
                    var x = "Les fitxes d'un sol cop es poden utilitzar com a autenticació secundària. Genereu un conjunt, imprimiu-los i guardeu-los en un lloc segur.";
                    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 hi ha fitxes actives";
                    }
                    x += '</table></div></div><br />';
                    x += '<div class="btn-group">';
                    x += '<button type=button class="btn btn-warning" onclick="account_manageOtp(1);">' + "Genera nous fitxes" + '</input>';
                    if (message.passwords != null) {
                        x += '<button type=button class="btn btn-danger" onclick="account_manageOtp(2);">' + "Esborrar fitxes" + '</button></div>';
                        x += '&nbsp;<i class="fa-regular fa-clipboard" title="' +  "Copieu els codis vàlids al porta-retalls" + '" style="cursor:pointer" onclick=copyTextToClip2("' + encodeURIComponentEx(clipb) + '")></i>';
                    } else {
                        x += '</div>'
                    }
                    xxdialogTag = 'otpauth-manage';
                    setModalContent('xxAddAgent', "Gestionar codis de seguretat", 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">' + "Les claus de maquinari" + '</a> ' + "s'utilitzen com a autenticació d'inici de sessió secundària.";
                    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 + ')>' + "Eliminar" + '</button>' + end;
                        }
                    } else {
                        x += start + '<tr style=text-align:center><td>' + "No s'ha configurat cap clau" + 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);">' + "Afegeix la clau" + '</input>'; }
                        if ((features & 0x00004000) != 0) { x += '<button class="btn btn-primary" id=d2addkey2 type=button onclick="account_addhkey(2);">' + "Afegeix YubiKey&reg; OTP" +'</input>'; }
                    } else {
                        x += "S'han assolit el màxim de claus.";
                    }
                    x += '</div><br />';
                    xxdialogTag = 'otpauth-hardware-manage';
                    setModalContent('xxAddAgent', "Gestionar les claus de seguretat", 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', "Afegeix clau de seguretat", '<br />' + "Error, no s'ha pogut afegir la clau." + '<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', "Afegeix clau de seguretat", '<br />' + "Error, no s'ha pogut afegir la clau." + '<br /><br />');
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');

                    }
                    break;
                }
                case 'webauthn-startregister': {
                    if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
                    var x = "Premeu el botó de la tecla ara." + '<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', "Afegeix clau de seguretat", 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', "Afegeix clau de seguretat", "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>' + "Comproveu el vostre telèfon i introduïu el codi de verificació.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Codi de verificació:" + ' <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', "Notificacions telefòniques", 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>' + "Comproveu la vostra aplicació de missatgeria i introduïu el codi de verificació.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Codi de verificació:" + ' <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', "Notificacions de missatgeria", 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("Ajuda sol·licitada 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 = "Agent connectat";
                                    if ((node.agent != null) && (node.agent.root === false)) { agentPrivilages = "Agent connectat amb privilegis limitats"; }
                                    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 connectat"; addNotification(abc); }
                                    if (((node.conn & 4) == 0) && ((message.event.conn & 4) != 0)) { abc.text = "Intel AMT connectat"; addNotification(abc); }
                                    if (((node.conn & 16) == 0) && ((message.event.conn & 16) != 0)) { abc.text = "MQTT connectat"; 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 = "Agent desconnectat"; addNotification(abc); }
                                    if (((node.conn & 2) != 0) && ((message.event.conn & 2) == 0)) { abc.text = "Intel AMT CIRA desconnectat"; addNotification(abc); }
                                    if (((node.conn & 4) != 0) && ((message.event.conn & 4) == 0)) { abc.text = "Intel AMT desconnectat"; addNotification(abc); }
                                    if (((node.conn & 16) != 0) && ((message.event.conn & 16) == 0)) { abc.text = "MQTT desconnectat"; 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 es pot escanejar aquest interval d'adreces." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Valors d'interval d'IP de mostra" + '<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 += " amb TLS."; } else { str += " sense 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>' + "L'escaneig no ha retornat cap resultat." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Valors d'interval d'IP de mostra" + '<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', "Cap");
                                }
                            }
                            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 ha pogut missatge d'esdeveniment:", 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 desats correctament");
                    } else {
                        alert("Error en desar els 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} hores", message.expire);
                    if (message.expire == 24) { t = "1 dia"; }
                    if (message.expire == 168) { t = "1 setmana"; }
                    if (message.expire == 5040) { t = "1 mes"; }
                    if (message.expire == 0) { t = "Il·limitat"; }
                    QH('agentInvitationLink', format("Enllaç d'invitació ({0})", t));
                    QV('agentInvitationLinkDiv', true);
                    break;
                }
                case 'createDeviceShareLink': { // Guest sharing link
                    var node = getNodeFromId(message.nodeid), x = '';
                    if (node == null) break;
                    x += addHtmlValue("Dispositiu", EscapeHtml(node.name));
                    x += addHtmlValue("Nom del convidat", message.guestname);
                    x += addHtmlValue("Entrada d'usuari", message.viewOnly ? "No es permet, només visualització" : "Permès");
                    if (message.start && message.expire) {
                        if (message.recurring) {
                            x += addHtmlValue("L'hora d'inici", printDateTime(new Date(message.start)));
                            if (message.expire < 2) { x += addHtmlValue("Durada", format("{0} minut", message.expire)); }
                            else { x += addHtmlValue("Durada", format("{0} minuts", message.expire)); }
                        } else {
                            x += addHtmlValue("L'hora d'inici", printDateTime(new Date(message.start)));
                            x += addHtmlValue("Temps d'expiració", printDateTime(new Date(message.expire)));
                        }
                    }
                    if (message.recurring == 1) { x += addHtmlValue("Recurrent", "Diàriament"); }
                    if (message.recurring == 2) { x += addHtmlValue("Recurrent", "Setmanalment"); }
                    var y = [];
                    if (message.consent & 0x0007) { y.push("Notificar"); }
                    if (message.consent & 0x0038) { y.push("Avís"); }
                    if (message.consent & 0x0040) { y.push("Barra de privadesa"); }
                    if (y.length == 0) { y.push("Cap"); }
                    x += addHtmlValue("Consentiment de l'usuari", y.join(', '));
                    var type = ''; if (message.p <= 7) { type = ['', "Enllaç de terminal remot", "Enllaç a l'escriptori remot", "Escriptori remot + Enllaç terminal", "Enllaç de fitxers remots", "Terminal remot + Enllaç de fitxers", "Escriptori remot + Enllaç de fitxers", "Escriptori remot + Terminal + Enllaç de fitxers"][message.p]; } else if (message.p == 8) { type = format("Enllaç HTTP/{0}", 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="' + "Copia l'enllaç al porta-retalls" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></i></div></div>';
                    setModalContent('xxAddAgent', "Comparteix el dispositiu", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getmqttlogin': {
                    if ((currentNode == null) || (currentNode._id != message.nodeid) || (xxdialogMode != null)) return;
                    var x = "Aquesta configuració es pot utilitzar per connectar MQTT per a aquest dispositiu." + '<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', "Credencials 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ó del connector", format('Select the version to downgrade the plugin: {0}', message.info.name) + '<hr />' + vSelect + '<hr />' + "Tingueu en compte que no es recomana la baixada. Feu-ho només en cas que una actualització recent hagi trencat alguna cosa." + + '<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>' + "Accedeix a l'URL següent, concedeix accés i torna a copiar el codi del testimoni." + '</div><br />';
                            x += addHtmlValue("Activació", '<a href=\"' + message.url + '\" rel="noreferrer noopener" target="_blank">Browse to this URL</a>');
                            x += addHtmlFormFloating("Codi", '<input id=gdcode onkeyup=server_setupGoogleDriveBackupCheck3() class=form-control></input>');
                            setModalContent('xxAddAgent', "Còpia de seguretat de Google Drive", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_setupGoogleDriveBackupEx(3, 'gd2'));
                            Q('gdcode').focus();
                            QE('idx_dlgOkButton', false);
                        }
                    }
                    break;
                }
                case 'pluginError': {
                    setModalContent('xxAddAgent', "Error del connector", 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 = "Inici de sessió vàlid"; 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àlid"; c = 'DD9DC3'; xx = 'x'; }
                            else if (m == 109) { m = "Compte bloquejat"; c = 'E1BBBB'; xx = 'x'; }
                            else if (m == 110) { m = "Contrasenya invà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', "Inicis de sessió anteriors", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getDeviceDetails': {
                    saveAs(new Blob([message.data], { type: 'application/octet-stream' }), "llista de dispositius" + '.' + message.type);
                    break;
                }
                case 'createLoginToken': { // A new login token was created
                    if (xxdialogMode) return;
                    var x = "Preneu nota d'aquest nom d'usuari i contrasenya, la contrasenya no es pot tornar a mostrar." + '<br /><br />';
                    x += addHtmlValue("Nom", EscapeHtml(message.name))
                    if (message.expire != 0) { x += addHtmlValue("Caduca", EscapeHtml(printDateTime(new Date(message.expire)))); }
                    x += addHtmlValue("Nom d'usuari", '<i class="fa-regular fa-copy" title="' + "Copia l'enllaç al porta-retalls" + '" 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("Contrasenya", '<i class="fa-regular fa-copy" title="' + "Copia l'enllaç al porta-retalls" + '" 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', "Login Token Created", x);
                    QV('idx_dlgOkButton', false);
                    Q('idx_dlgCancelButton').innerHTML = "Tanca";
                    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' : '') + '>' + "Tipus d'agent" + '</label><br />';
            x += '<label><input id=d2c9 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('agver') >= 0) ? ' checked' : '') + '>' + "Versió de l'agent" + '</label><br />';
            x += '<label><input id=d2c6 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('desc') >= 0) ? ' checked' : '') + '>' + "Descripció del dispositiu" + '</label><br />';
            x += '<label><input id=d2c5 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('os') >= 0) ? ' checked' : '') + '>' + "Sistema operatiu" + '</label><br />';
            x += '<label><input id=d2c10 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('tags') >= 0) ? ' checked' : '') + '>' + "Etiquetes" + '</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' : '') + '>' + "actualització de Windows" + '</label><br />';
            x += '<label><input id=d2c13 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) ? ' checked' : '') + '>' + "Firewall de Windows" + '</label><br />';
            x += '<label><input id=d2c14 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) ? ' checked' : '') + '>' + "Última hora d'arrencada" + '</label><br />';
            x += '<label><input id=d2c16 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('idletime') >= 0)?' checked':'') + '>' + "Temps d'inactivitat" + '</label><br />';
            x += '<label><input id=d2c1 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('links') >= 0) ? ' checked' : '') + '>' + "Enllaços d'encaminador MeshCentral" + '</label><br />';
            x += '<label><input id=d2c2 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('user') >= 0) ? ' checked' : '') + '>' + "Usuaris connectats" + '</label><br />';
            x += '<label><input id=d2c3 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('ip') >= 0) ? ' checked' : '') + '>' + "Adreça IP de l'agent" + '</label><br />';
            x += '<label><input id=d2c4 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('conn') >= 0) ? ' checked' : '') + '>' + "Connectivitat del servidor" + '</label><br />';
            x += '<label><input id=d2c15 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amthost') >= 0) ? ' checked' : '') + '>' + "Nom d'amfitrió Intel&reg; AMT" + '</label><br />';
            x += '<label><input id=d2c17 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amtstate') >= 0) ? ' checked' : '') + '>' + "Estat Intel&reg; AMT" + '</label><br />';
            x += '<label><input id=d2c7 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastseen') >= 0) ? ' checked' : '') + '>' + "Vist per últim cop" + '</label><br />';
            setModalContent('xxAddAgent', "Columnes de visualització del dispositiu", 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>' + ", només Intel&reg; AMT" + '</span>'; }
                            if (meshes[node.meshid] && (meshes[node.meshid].mtype == 3)) { if (meshes[node.meshid].relayid) { extra = '<span class=devHeaderx>' + ", Dispositius retransmesos" + '</span>'; } else { extra = '<span class=devHeaderx>' + ", Dispositius locals" + '</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>' + "Dispositius individuals" + '</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>' + "Dispositius individuals" + '</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>' + "Dispositius individuals" + '</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 s'inclou cap dispositiu a cap grup, feu clic a \"Grups\" d'un dispositiu per afegir-lo a un grup." + '</div>';
                    } else {
                        r = '<div style="margin:30px">' + "No hi ha cap dispositiu que coincideixi amb aquesta cerca." + ' <a onclick=clearDeviceSearch()>' + "Esborra el filtre de cerca" + '</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 hi ha dispositius Intel&reg; AMT en aquest grup de dispositius";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "afegiu-ne un" + '</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 hi ha dispositius en aquest grup de dispositius";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "afegiu-ne un" + '</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 hi ha dispositius locals en aquest grup de dispositius";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "afegiu-ne un" + '</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 hi ha dispositius en aquest grup de dispositius";
                            }
                            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>' + "Tipus d'agent"; }
                        if (deviceViewSettings.devsCols.indexOf('agver') >= 0) { colums += '<th style=width:100px>' + "Versió de l'agent"; }
                        if (deviceViewSettings.devsCols.indexOf('os') >= 0) { colums += '<th style=width:160px>' + "OS"; }
                        if (deviceViewSettings.devsCols.indexOf('tags') >= 0) { colums += '<th style=width:120px>' + "Etiquetes"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsav') >= 0) { colums += '<th style=width:100px>' + "Windows AV"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) { colums += '<th style=width:120px>' + "actualització de Windows"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) { colums += '<th style=width:120px>' + "Firewall de Windows"; }
                        if (deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) { colums += '<th style=width:120px>' + "Última hora d'arrencada"; }
                        if (deviceViewSettings.devsCols.indexOf('idletime') >= 0) { colums += '<th style=width:100px title="' + "Actualitzat cada 5 minuts" + '">' + "Temps d'inactivitat"; }
                        if (deviceViewSettings.devsCols.indexOf('links') >= 0) { colums += '<th style=width:120px>' + "Enllaços"; }
                        if(features3 & 0x00000002){
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:180px>' + "Usuari"; }
                        } else {
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:120px>' + "Usuari"; }
                        }
                        if (deviceViewSettings.devsCols.indexOf('ip') >= 0) { colums += '<th style=width:120px>' + "adreça"; }
                        if (deviceViewSettings.devsCols.indexOf('conn') >= 0) { colums += '<th style=width:100px>' + "Connectivitat"; }
                        if (deviceViewSettings.devsCols.indexOf('amthost') >= 0) { colums += '<th style=width:100px>' + "Amfitrió AMT"; }
                        if (deviceViewSettings.devsCols.indexOf('amtstate') >= 0) { colums += '<th style=width:100px>' + "Estat AMT"; }
                        if (deviceViewSettings.devsCols.indexOf('lastseen') >= 0) {
                            colums += '<th style=width:120px>' + "Vist per últim cop";
                            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 s'ha trobat cap dispositiu amb etiquetes." + '</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="' + "Creeu un nou grup de dispositius." + '" style=cursor:pointer>' + "Afegeix un grup de dispositius" + '</a>&nbsp';
                }
                if ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 128) == 0)) {
                    r += '<a href=# onclick=\'return p10showMeshCmdDialog(0)\' style=cursor:pointer title="' + "Descarregueu MeshCmd, una eina de línia d'ordres que realitza moltes funcions." + '">' + "MeshCmd" + '</a>&nbsp';
                    if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) { r += '<a href=# onclick=\'return p10showMeshRouterDialog()\' style=cursor:pointer title="' + "Descarregueu MeshCentral Router, una eina de mapatge de ports TCP." + '">' + "Encaminador" + '</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, ["Desconnectat", "Connectant...", "Configuració...", '', ''][((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, ["Desconnectat", "Connectant...", "Configuració...", '', ''][((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>' + "Cap" + '</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>' + "Cap" + '</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 dispositiu està connectat"; }
                    if (bat.state == 'dc') { statestr = "El dispositiu funciona amb bateria"; }

                    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 port del commutador està llest per utilitzar-lo." + '">' + "Interruptor" + '</span>');
                            } else {
                                states.push('<span title="' + "El port IP-KVM està connectat i llest per al seu ús." + '">' + "IP-KVM" + '</span>');
                            }
                        } else {
                            states.push('<span title="' + "L'agent de malla està connectat i llest per al seu ús." + '">' + "Agent" + '</span>');
                        }
                    }
                    if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA està connectat i llest per al seu ús." + '">' + "CIRA" + '</span>'); }
                    else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT és encaminable." + '">' + "AMT" + '</span>'); }
                    if ((node.conn & 8) != 0) { states.push('<span title="' + "L'agent de malla és accessible mitjançant un altre agent com a relé." + '">' + "Relleu" + '</span>'); }
                    if ((node.conn & 16) != 0) { states.push('<span title="' + "La connexió MQTT al dispositiu està activa." + '">' + "MQTT" + '</span>'); }
                }
                if (node.mtype == 3) {
                    var mesh = meshes[node.meshid];
                    if (mesh && mesh.relayid) {
                        states.push('<span title="' + "Connexió de xarxa local mitjançant un agent de retransmissió." + '">' + "Relleu" + '</span>');
                    } else {
                        states.push('<span title="' + "Connexió a la xarxa 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>' + "D'acord" + '</span>' : '<span style=color:red>' + "DOLENT" + '</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>' + "D'acord" + '</span>' : '<span style=color:red>' + "DOLENT" + '</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>' + "D'acord" + '</span>' : '<span style=color:red>' + "DOLENT" + '</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 no està activat" + '">' + "Pre" + '</span>'; }
                        if (node.intelamt.state == 1) { amtstate = ' <span title="' + "Intel&reg; AMT està en mode d'activació" + '">' + "En" + '</span>'; }
                        if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { amtstate = ' <span title="' + "Intel&reg; AMT està activat en mode de control de client" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { amtstate += ' <span title="' + "Intel&reg; AMT està activat en mode de control d'administració" + '">' + "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 += "Connectat"; } 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 + '\')">' + "Desconnectat" + '</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', "Sol·licituds d'ajuda" + ' - ' + 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>' + "Escriptori remot" + '</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="' + "Desconnecteu aquesta sessió" + '" class="fa-fw fa-solid fa-trash text-danger" ></i>'; }
                            x += addHtmlValue2(getUserName(j), ((node.sessions.kvm[j] == 1) ? "1 sessió" : nobreak(format("{0} sessions", node.sessions.kvm[j]))) + trash);
                        } else if (j == 'busy') {
                            x += addHtmlValue2("El dispositiu està ocupat", ((node.sessions.kvm[j] == 1) ? "1 sessió" : nobreak(format("{0} sessions", node.sessions.kvm[j]))));
                        }
                    }
                } else if (i == 'multidesk') {
                    x += '<u>' + "Escriptori remot" + '</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="' + "Desconnecteu aquesta sessió" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.multidesk[j] == 1) ? "1 sessió" : nobreak(format("{0} sessions", 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="' + "Desconnecteu aquesta sessió" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.terminal[j] == 1) ? "1 sessió" : nobreak(format("{0} sessions", node.sessions.terminal[j]))) + trash);
                    }
                } else if (i == 'files') {
                    x += '<u>' + "Fitxers" + '</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="' + "Desconnecteu aquesta sessió" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.files[j] == 1) ? "1 sessió" : nobreak(format("{0} sessions", node.sessions.files[j]))) + trash);
                    }
                } else if (i == 'tcp') {
                    x += '<u>' + "Enrutament 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="' + "Desconnecteu aquesta sessió" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.tcp[j] == 1) ? "1 sessió" : nobreak(format("{0} sessions", node.sessions.tcp[j]))) + trash);
                    }
                } else if (i == 'udp') {
                    x += '<u>' + "Encaminament 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="' + "Desconnecteu aquesta sessió" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.udp[j] == 1) ? "1 sessió" : nobreak(format("{0} sessions", node.sessions.udp[j]))) + trash);
                    }
                }
            }
            if (x != '') {
                xxdialogTag = 'SESSIONS-' + node._id;
                setModalContent('xxAddAgent', "Sessions" + ' - ' + 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', "Missatges de l'agent" + ' - ' + 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} usuaris", 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) + ' (' + "Tancat" + ')', (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', "Connecta-ho tot", format("Esteu segur que voleu connectar-vos a {0} dispositius?", 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, ["Desconnectat", "Connectant...", "Configuració...", '', ''][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', "Configuració de l'escriptori remot", 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="' + "Afegiu un nou ordinador Intel&reg; AMT que es trobi a la xarxa local." + '" onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "Afegeix local" + '</a>';
                    r += ' <a href=# role="button" title="' + "Afegiu un nou ordinador Intel&reg; AMT escanejant la xarxa local." + '" onclick=\'return addAmtScanToMesh("' + mesh._id + '")\'>' + "Escaneja la xarxa" + '</a>';
                }
                if (mesh.amt && (mesh.amt.type > 0)) { // CCM Deactivate, CCM or ACM activation, Full Automatic
                    r += ' <a href=# role="button" title="' + "Realitzeu l'activació i la configuració d'Intel&reg; AMT." + '" onclick=\'return showAmtSetup("' + mesh._id + '")\'>' + "Configuració" + '</a>';
                }
            }
            if ((mesh.mtype == 2) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { // Agent device group
                r += ' <a href=# role="button" title="' + "Afegiu un ordinador nou a aquest grup de dispositius instal·lant l'agent de malla." + '" onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "Afegeix un agent" + '</a>';
                if ((features & 2) == 0) { r += ' <a href=# role="button" title="' + "Convida algú a instal·lar l'agent de malla en aquest grup de dispositius." + '" onclick=\'return inviteAgentToMesh("' + mesh._id + '")\'>' + "Convidar" + '</a>'; }
            }
            if (mesh.mtype == 3) { // Local device group
                r += ' <a href=# role="button" title="' + "Afegeix un dispositiu situat a la xarxa local." + '" onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "Afegeix un dispositiu" + '</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("Afegiu un dispositiu local al grup de dispositius \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Nom del dispositiu", '<input id=dp1devicename class=form-control maxlength=32 autocomplete=off onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Nom d'amfitrió", '<input id=dp1hostname class=form-control maxlength=32 autocomplete=off placeholder="' + "Igual que el nom del dispositiu" + '" onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Tipus", '<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', "Afegeix un dispositiu 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("Afegiu un dispositiu Intel&reg; AMT nou al grup de dispositius \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Nom del dispositiu", '<input id=dp1devicename class="form-control" maxlength=32 autocomplete=off onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Nom d'amfitrió", '<input id=dp1hostname class="form-control" maxlength=32 autocomplete=off placeholder="' + "Igual que el nom del dispositiu" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Nom d'usuari", '<input id=dp1username class="form-control" maxlength=32 autocomplete=off placeholder="' + "admin" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Contrasenya", '<input id=dp1password type=password class="form-control" autocomplete=off maxlength=32 onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Seguretat", '<select id=dp1tls class="form-select"><option value=0>' + "Sense seguretat TLS" + '</option><option value=1>' + "Es requereix seguretat TLS" + '</option></select>');

            setModalContent('xxAddAgent', "Afegeix un dispositiu 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 = "Creeu molts dispositius Intel&reg; AMT alhora important un fitxer JSON amb el format següent:" + '<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', "Importació de dispositius 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', "Importació de dispositius Intel&reg; AMT", format("Fitxer JSON no vàlid: {0}.", ex));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    return;
                }
                if (j != null) {
                    meshserver.send({ action: 'importamtdevices', meshid: meshid, amtdevices: j });
                } else {
                    setModalContent('xxAddAgent', "Importació de dispositius Intel&reg; AMT", "Format de fitxer JSON no vàlid.");
                    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("Afegiu, activeu i configureu Intel&reg; AMT per agrupar \"{0}\" executant periòdicament MeshCmd com a administrador al dispositiu remot.", 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>' + "Per a l'activació d'ACM, Intel&reg; AMT s'haurà de configurar amb el FQDN de confiança següent:" + ' <b>' + serverinfo.amtAcmFqdn.join(', ') + '</b></div>');
            }
            setModalContent('xxAddAgent', "Configuració d'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>' + "Activeu Intel&reg; AMT en mode de control d'administració (ACM) mitjançant una clau USB amb format FAT. Col·loqueu-hi setup.bin i arrenqueu un o més ordinadors amb aquesta clau." + '</div><div style=margin-top:6px>' + "Comenceu introduint la contrasenya MEBx antiga i nova." + '</div></table>';
            x += addHtmlFormFloating("contrasenya antiga", '<input id=dp1password0 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Nova contrasenya*", '<input id=dp1password1 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Nova contrasenya*", '<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" /> ' + "S'utilitza per a l'activació de la LAN de metall nu." + '</label>'; } // Intel AMT LAN provisioning server is active.
            x += '<div><span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 caràcters, 1 superior, 1 inferior, 1 numèric, 1 no alfanumèric." + '</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 = "Introduïu un rang d'adreces IP per buscar dispositius 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("Interval 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="' + "Escaneja" + '" onclick=addAmtScanToMeshButton()></input></tr></table>');
            } else {
                x += addHtmlValue("Interval 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="' + "Escaneja" + '" 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="' + "Cancel · lar" + '" style="" onclick="dialogclose(0)" /><input id="idx_dlgOkButton2" type="button" value="OK" style="" onclick="dialogclose(1)" /><div><input id="d2scanSelectButton" type="button" value="' + "Seleccionar tot" + '" onclick="scanSelectAll()" /></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Busca dispositius 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>' + "Valors d'interval d'IP de mostra" + '<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>' + "Escanejant..." + '</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("Tipus d'invitació", '<select id=d2InviteType onchange=d2ChangedInviteType() class=form-select><option value=0>' + "Invitació d'enllaç" + '</option><option value=1>' + "Invitació per correu electrònic" + '</option></select>') + '<hr />';
                x += '<div id=emailInviteDiv style=display:none>' + format("Convida algú a instal·lar l'agent de malla. S'enviarà un correu electrònic amb l'enllaç a la instal·lació de l'agent de malla per al grup de dispositius \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
                x += addHtmlFormFloating("Nom (opcional)", '<input id=agentInviteName value="" placeholder="Name (optional)" class="form-control" maxlength=64 />');
                x += addHtmlFormFloating("Correu electrònic", '<input id=agentInviteEmail placeholder="' + "exemple@email.com" + '" class="form-control" onkeyup=validateAgentInvite() />');
                x += addHtmlFormFloating("Sistema operatiu", '<select id=agentInviteNameOs onchange=d2ChangedInviteType() class=form-select><option value=4>' + "Envia l'enllaç d'instal·lació" + '</option><option value=0 selected>' + "Qualsevol suportat" + '</option><option value=1>' + "Només Windows" + '</option><option value=3>' + "Només Apple macOS" + '</option><option value=2>' + "Només Linux" + '</option><option value=5>' + "Assistent MeshCentral" + '</option></select>');
                x += '<div id=d2agentexpirediv>';
                x += addHtmlFormFloating("Caducitat de l'enllaç", '<select id=agentInviteExpire class=form-select><option value=1>' + "1 hora" + '</option><option value=8>' + "8 hores" + '</option><option value=24>' + "1 dia" + '</option><option value=168>' + "1 setmana" + '</option><option value=5040>' + "1 mes" + '</option><option value=0>' + "Il·limitat" + '</option></select>');
                x += '</div>';
                x += '<div id=d2agentInstallTypeDiv2>';
                x += addHtmlFormFloating("Tipus d'instal·lació", '<select id=agentInviteType class=form-select><option value=0>' + "Fons i interactiu" + '</option><option value=2>' + "Només fons" + '</option><option value=1>' + "Només interactiu" + '</option></select>');
                x += '</div>';
                x += addHtmlFormFloating("Missatge" + ' ' + "(opcional)", '<textarea id="agentInviteMessage" class="form-control" style="height:100px;resize:none;" maxlength="1024"></textarea>');
                x += '</div>';
            }
            x += '<div id=urlInviteDiv>' + format("Convideu algú a instal·lar l'agent de malla compartint un enllaç d'invitació. Aquest enllaç dirigeix ​​l'usuari a les instruccions d'instal·lació del grup de dispositius \"{0}\". L'enllaç és públic i no cal cap compte per a aquest servidor.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Caducitat de l'enllaç", '<select id=d2inviteExpire class=form-select onchange=d2RequestInvitationLink()><option value=1>' + "1 hora" + '</option><option value=8>' + "8 hores" + '</option><option value=24>' + "1 dia" + '</option><option value=168>' + "1 setmana" + '</option><option value=5040>' + "1 mes" + '</option><option value=0>' + "Il·limitat" + '</option></select>');
            x += addHtmlFormFloating("Agents", '<select id=d2agentType class=form-select onchange=d2ChangedInviteType()><option value=0>' + "Tots els agents disponibles" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "Assistent MeshCentral" + '</option><option value=16>' + "MeshAgent d'Android" + '</option></select>');
            x += '<div id=d2agentInstallTypeDiv>';
            x += addHtmlFormFloating("Tipus d'instal·lació", '<select id=d2agentInviteType class=form-select onchange=d2RequestInvitationLink()><option value=0>' + "Fons i interactiu" + '</option><option value=2>' + "Només fons" + '</option><option value=1>' + "Només interactiu" + '</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="' + "Copia l'enllaç al porta-retalls" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Convidar", 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>' + "Instal·lador binari de Linux / BSD / macOS" + '</option><option value=2>' + "Apple macOS" + '</option>';
            if ((features & 2) == 0) { opts += '<option value=6>' + "Dispositiu mòbil" + '</option>'; } // Don't display mobile setup in LAN mode.
            opts += '<option value=7>' + "Assistent MeshCentral" + '</option>';
            opts += '<option value=3>' + "Windows (Desinstal·lació)" + '</option><option value=4>' + "Linux/BSD (Desinstal·lació)" + '</option><option value=8>' + "Apple macOS (desinstal·lació)" + '</option></select>';
            x += addHtmlFormFloating("Sistema operatiu", 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("Tipus 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("Tipus d'instal·lació", '<select id="aginsType" onchange="addAgentToMeshClick()" class="form-select"><option value="0">' + "Fons i interactiu" + '</option><option value="2">' + "Només fons" + '</option><option value="1">' + "Només interactiu" + '</option></select>');
            x += '</div><div id="asinsTypeDiv">';
            x += addHtmlFormFloating("Tipus d'instal·lació", '<select id="asinsType" onchange="addAgentToMeshClick()" class="form-select"><option value="2">' + "Aplicació, connectar-se a petició de l'usuari" + '</option><option value="3">' + "Aplicació, sempre connectat" + '</option><option value="0">' + "Safata del sistema, connecteu-vos a petició de l'usuari" + '</option><option value="1">' + "Safata del sistema, sempre connectat" + '</option><option value="4">' + "Safata del sistema, només monitor" + '</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("Per afegir un ordinador nou al grup de dispositius \"{0}\", descarregueu l'agent de malla i instal·leu-lo a l'ordinador per gestionar-lo. Aquest agent té incrustada informació del servidor i del grup de dispositius.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline id=aginsw32lnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Versió x86 de 32 bits del MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de Windows x86 de 32 bits al porta-retalls" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline id=aginsw64lnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Versió x86 de 64 bits del MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de Windows x86 de 64 bits al porta-retalls" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline id=aginswa64lnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Versió ARM de 64 bits del MeshAgent" + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de Windows ARM de 64 bits al porta-retalls" + '" 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("Fitxer de configuració", '<a id=aginswmshlnk name="meshsettings?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + format("Configuració de {0} (.msh)", EscapeHtml(mesh.name)) + '</a>'); }
            x += '</div>';

            // Linux agent install
            x += '<div id=agins_linux style=display:none>' + format("Per afegir un ordinador a \"{0}\" executeu l'ordre següent. Es necessitaran credencials d'arrel.", 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>' + "* Per a BSD, executeu primer \"pkg install wget sudo bash\"." + '</div><a style=text-decoration:none title="' + "Copiar al portapapers" + '" onclick=copyAgentIdValue("agins_linux_area")>' + "Còpia" + ' <i class="fa-regular fa-clipboard" style=cursor:pointer></i></a></div>';

            // macOS agent install
            x += '<div id=agins_osx style=display:none>' + format("Per afegir un ordinador nou al grup de dispositius \"{0}\", descarregueu l'agent de malla i instal·leu-lo a l'ordinador per gestionar-lo. Aquest instal·lador d'agent té incrustada informació del servidor i del grup de dispositius.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Agent de malla", '<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="' + "Copieu l'URL de l'agent de macOS al porta-retalls" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=16&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agent de malla", '<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 (64 bits)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de macOS al porta-retalls" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=29&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Versió universal de macOS Mesh Agent" + '">' + "macOS (Universal)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de macOS al porta-retalls" + '" 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("Per afegir un dispositiu mòbil al grup \"{0}\", baixa l'aplicació MeshAgent i escaneja aquest codi 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("APK d'Android", '<a class=text-decoration-underline onclick=downloadFile("meshagents?id=14' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '",null,true) title="' + "Versió APK de MeshAgent" + '">' + "APK" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copia l'URL al porta-retalls" + '" 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("MeshCentral Assistant és una eina de Windows que els usuaris poden utilitzar per demanar ajuda. Utilitzeu l'enllaç següent per baixar una versió que es connecti al grup de dispositius \"{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="' + "MeshCentral Assistant per a Windows" + '">' + "Assistent per a Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copia l'URL al porta-retalls" + '" 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>' + "MeshCentral Assistant és una eina de Windows que els usuaris poden utilitzar per demanar ajuda. Utilitzeu l'enllaç següent per descarregar una versió que supervisarà l'agent en segon pla." + '</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="' + "MeshCentral Assistant per a Windows" + '">' + "Assistent per a Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copia l'URL al porta-retalls" + '" 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>' + "Per eliminar un agent de malla, descarregueu el fitxer següent, executeu-lo i feu clic a \"desinstal·la\"." + '<br /><br />';
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline id=aginsw32unlnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Versió x86 de 32 bits del MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de Windows x86 de 32 bits al porta-retalls" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline id=aginsw64unlnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Versió x86 de 64 bits del MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de Windows x86 de 64 bits al porta-retalls" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline id=aginswa64unlnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "Versió ARM de 64 bits del MeshAgent" + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de Windows ARM de 64 bits al porta-retalls" + '" 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>' + "Per eliminar un agent de malla, executeu l'ordre següent. Es necessitaran credencials d'arrel." + '<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 portapapers" + '" 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>' + "Per eliminar un agent de malla, descarregueu el fitxer següent, extraieu l'arxiu ZIP i, a continuació, feu clic amb el botó dret a \"Uninstall.command\" i seleccioneu \"Obrir\"." + '<br /><br />';
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Versió universal de macOS Mesh Agent" + '">macOS Agent (Universal)</a> <i class="fa-regular fa-clipboard" title="' + "Copieu l'URL de l'agent de macOS al porta-retalls" + '" 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>' + "Aquest és un executable en sistemes operatius amb interfícies d'usuari gràfiques." + '<br /><br />' + "Els executables d'Apple macOS s'hauran d'eliminar de la quarantena per executar 'xattr -r -d com.apple.quarantine meshagent'." + '<br /><br />' + "Heu de 'chmod +x meshagent' i executar aquest fitxer." + '<br /><br />';
            x += addHtmlValue("Agent de malla", '<a class=text-decoration-underline id=aginsbinlnk name="meshagents?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + "agent de malla" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copia l'URL de l'agent al porta-retalls" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Comandament" + '&nbsp;<i class="fa-regular fa-clipboard" title="' + "Copia l'URL de l'agent al porta-retalls" + '" 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', "Afegeix un agent de malla", 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 node" : format("{0} nodes", 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 dispositiu està encès." + '">' + "Alimentat" + '</span>', '<span title="' + "El dispositiu està en estat de repòs (S1)." + '">' + "Dorment" + '</span>', '<span title="' + "El dispositiu està en estat de repòs (S2)." + '">' + "Dorment" + '</span>', '<span title="' + "El dispositiu està en estat de son profund (S3)." + '">' + "El somni profund" + '</span>', '<span title="' + "El dispositiu està en estat d'hibernació (S4)." + '">' + "Hibernant" + '</span>', '<span title="' + "El dispositiu està en estat apagat (S5)." + '">' + "Soft-Off" + '</span>', '<span title="' + "S'ha detectat el dispositiu, però no s'ha pogut obtenir l'estat d'alimentació." + '">' + "Present" + '</span>', '<span title="' + "El dispositiu està apagat." + '">' + "Apagat" + '</span>'];
        var powerStateStrings2 = ['', "El dispositiu està alimentat", "El dispositiu està en estat de repòs (S1)", "El dispositiu està en estat de repòs (S2)", "El dispositiu està en estat de repòs profund (S3)", "El dispositiu està hibernant (S4)", "El dispositiu està en estat d'apagat suau (S5)", "El dispositiu està present, però no es pot determinar l'estat d'alimentació", "El dispositiu està apagat"];
        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="' + "L'interruptor d'alimentació està llest per al seu ús." + '">' + "Interruptor" + '</span>');
                        } else {
                            states.push('<span title="' + "El port IP-KVM està disponible i llest per utilitzar-lo." + '">' + "IP-KVM" + '</span>');
                        }
                    } else {
                        states.push('<span title="' + "L'agent de malla està connectat i llest per al seu ús." + '">' + "Agent" + '</span>');
                    }
                }
                if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA està connectat i llest per al seu ús." + '">' + "CIRA" + '</span>'); }
                else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT és encaminable." + '">' + "AMT" + '</span>'); }
                if ((node.conn & 8) != 0) { states.push('<span title="' + "L'agent de malla és accessible mitjançant un altre agent com a relé." + '">' + "Relleu" + '</span>'); }
                if ((node.conn & 16) != 0) { states.push('<span title="' + "La connexió MQTT al dispositiu 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 "Desconegut";
        }

        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 = "Seleccioneu Cap";
                QV('cxmdesktop', true);
            } else {
                QE('GroupActionButton', false);
                Q('SelectAllButton').value = "Seleccionar tot";
                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', "Seguretat del compte", "No es pot accedir a aquesta funció fins que s'hagi activat l'autenticació de dos factors. Això és necessari per a una seguretat addicional. Aneu a la pestanya \"El meu compte\" i mireu la secció \"Seguretat del compte\".");
                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: "Envia un missatge MQTT" });; break; } }
            }

            // Display the options that are allowed depending on what devices are selected.
            addedOptions.push({ v: 105, name: "Exporta la informació del dispositiu", 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: "Mou al grup de dispositius" }); }
                if (((node.conn & 1) != 0) && ((rights & 0x8000) != 0) && ((added & 1) == 0)) { added |= 1; addedOptions.push({ v: 104, name: "Desinstal·leu l'agent" }); }
                if ((rights & 64) && ((added & 8) == 0)) { added |= 8; addedOptions.push({ v: 100, name: "Dispositius de despertador" }); }
                if ((rights & 262144) && ((added & 4) == 0)) { added |= 4; addedOptions.push({ v: 4, name: "Dispositius per dormir" }); addedOptions.push({ v: 3, name: "Restablir dispositius" }); addedOptions.push({ v: 2, name: "Apagueu els dispositius" }); }
                if ((rights & 131072) && ((added & 16) == 0)) { added |= 16; addedOptions.push({ v: 106, name: "Executeu ordres" }); }
                if ((rights & 16384) && ((added & 32) == 0)) { added |= 32; addedOptions.push({ v: 108, name: "Notificació del dispositiu" }); }
                if ((rights & 4) && ((added & 64) == 0)) { added |= 64; addedOptions.push({ v: 107, name: "Edita les etiquetes" }); }
                if ((rights & 8) && ((added & 256) == 0)) { added |= 256; addedOptions.push({ v: 109, name: "Carregueu fitxers" }); }
                if ((rights & 32768) && ((added & 128) == 0)) { added |= 128; addedOptions.push({ v: 101, name: "Suprimir dispositius" }); }
                if ((node.agent != null) && (features2 & 0x00000010) && (rights == 0xFFFFFFFF) && ((added & 512) == 0)) {
                    added |= 512;
                    addedOptions.push({ v: 110, name: "Força l'actualització de l'agent" });
                    addedOptions.push({ v: 111, name: "Nucli de l'agent clar" });
                    addedOptions.push({ v: 112, name: "Carregueu el nucli del servidor predeterminat" });
                }
            }

            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 = "Seleccioneu una operació per dur a terme en tots els dispositius seleccionats. Les accions es realitzaran només amb els drets corresponents." + '<br /><br />';
            x += addHtmlFormFloating("Funcionament", '<select id=d2groupop class="form-select">' + addedOptionsStr + '</select>');

            setModalContent('xxAddAgent', "Acció grupal", 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 += "Vols confirmar l'eliminació del dispositiu seleccionat?" + '<br /><br />'; }
                else {
                    x += format("Vols confirmar l'eliminació de {0} dispositius seleccionats?" + '<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 dispositiu seleccionat està en línia." + '<br />', nconn); }
                    else if (nconn > 1) { y += format("{0} dispositius seleccionats estan en línia." + '<br />', nconn); }
                    if (ndisc == 1) { y += format("1 dispositiu seleccionat està fora de línia." + '<br />', ndisc); }
                    else if (ndisc > 1) { y += format("{0} dispositius seleccionats estan fora de línia." + '<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() />' + "Confirmeu" + '</label>';
                setModalContent('xxAddAgent', "Suprimeix els dispositius", 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: "Executeu ordres als dispositius seleccionats.", func: uncheckAllDevices });
            } else if (op == 107) {
                // Edit tags
                var x = "Realitzeu l'operació d'etiquetes del dispositiu per lots" + '<br /><br />';
                x += addHtmlFormFloating("Funcionament", '<select id=d2deviceop class="form-select"><option value=1>' + "Afegeix etiquetes" + '</option><option value=2>' + "Estableix etiquetes" + '</option><option value=3>' + "Elimina les etiquetes" + '</option></select>');
                x += addHtmlFormFloating("Etiquetes", '<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', "Edita les etiquetes del dispositiu", x);
                $('#dp10devicevalue').select2({
                    theme: 'bootstrap-5',
                    width: $( this ).data( 'width' ) ? $( this ).data( 'width' ) : $( this ).hasClass( 'w-100' ) ? '100%' : 'style',
                    placeholder: "Etiqueta 1, etiqueta 2, etiqueta 3",
                    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>'+ "Realitzar notificacions per lots del dispositiu" + '</div>';
                x += '<select id=d2deviceop class="form-select-sm me-2" style=width:100%;margin-bottom:4px><option value=2>' + "Notificació de brindis" + '</option><option value=1>' + "Caixa de missatges" + '</option><option value=3>' + "Caixa d'alerta" + '</option></select>';
                x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Títol" + '" 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>' + "Mostra durant 2 minuts (per defecte)" + '</option>';
                x += '<option value=10>' + "Mostra durant 10 minuts" + '</option>';
                x += '<option value=30>' + "Mostra durant 30 minuts" + '</option>';
                x += '<option value=60>' + "Mostra durant 60 minuts" + '</option>';
                x += '<option value=0>' + "Mostra el missatge fins que l'usuari l'elimini" + '</option>';
                x += '</select>';
                setModalContent('xxAddAgent', "Notificació del dispositiu", 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 = "Carregueu els fitxers seleccionats a tots els dispositius seleccionats" + '<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 />' + "Creeu una carpeta si no existeix?" + '</label><br />';
                x += '<label><input type=checkbox class="form-check-input me-2" name=overwriteFiles />' + "Sobreescriure si el fitxer existeix?" + '</label></form>';
                setModalContent('xxAddAgent', "Càrrega de fitxers per lots", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2batchUploadValidateOk());
                d2batchUploadValidate();
            } else if (op == 110) {
                // Force agent update
                var x = "Voleu l'actualització de l'agent als dispositius seleccionats?" + '<br /><br />';
                setModalContent('xxAddAgent', "Força l'actualització de l'agent", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentUpdateExec());
            } else if (op == 111) {
                // Clear agent core
                var x = "Vols esborrar el nucli de l'agent als dispositius seleccionats?" + '<br /><br />';
                setModalContent('xxAddAgent', "Nucli de l'agent clar", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentClearCoreExec());
            } else if (op == 112) {
                // Upload default server core
                var x = "Vols penjar el nucli del servidor predeterminat als dispositius seleccionats?" + '<br /><br />';
                setModalContent('xxAddAgent', "Carregueu el nucli del servidor predeterminat", 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 = "Baixeu la llista de dispositius amb un dels formats de fitxer següents." + '<br /><br />';
            x += addHtmlValue("Format CSV", '<a href=# style=cursor:pointer onclick=\'return p2downloadDeviceInfoCSV()\'>' + 'devicelist.csv' + '</a>');
            x += addHtmlValue("Format 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 />' + "Inclou els detalls del dispositiu" + '</label></div>';
            setModalContent('xxAddAgent', "Exportació d'informació del dispositiu", 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' : '') + '>' + "Símbol del sistema de comandes de Windows" + '</option><option value=2' + ((runopt.type == 2) ? ' selected' : '') + '>' + "Windows PowerShell" + '</option>'; }
                if (linuxtype == true) { x += '<option value=3' + ((runopt.type == 3) ? ' selected' : '') + '>' + "Linux/BSD/macOS Command Shell" + '</option>'; }
                if (agenttype == true) { x += '<option value=4' + ((runopt.type == 4) ? ' selected' : '') + '>' + "Consola de l'agent" + '</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' : '') + '>' + "Executar com a agent" + '</option><option value=1' + ((runopt.runAs == 1) ? ' selected' : '') + '>' + "Executeu com a usuari, agent si no hi ha usuari" + '</option><option value=2' + ((runopt.runAs == 2) ? ' selected' : '') + '>' + "S'ha d'executar com a usuari" + '</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' : '') + '>' + "Ordres del quadre de text" + '</option><option value=1' + ((runopt.source == 1) ? ' selected' : '') + '>' + "Ordres del fitxer" + '</option>';
                    if (userinfo.siteadmin & 8) { x += '<option value=2' + ((runopt.source == 2) ? ' selected' : '') + '>' + "Ordres del fitxer al 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', "Executeu ordres", 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(' ' + "i" + ' '), 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 = "Port de connexió remota 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', "Connexió 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 = "Port de connexió 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', "Connexió 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 = "Port de connexió 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', "Connexió 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 = "Port de connexió remot 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', "Connexió 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 = "Port de connexió 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', "Connexió 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', "Canvia el nom", '<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 fitxers", "Només es poden editar fitxers de menys de 200 k."); }
            } else if (action == 3) {
                // Delete the file
                setModalContent('xxAddAgent', "Suprimeix", "Vols suprimir l'element?");
                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 la ubicació del node", callback: function (obj) { modifyMarkerloc(obj.data); } };

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

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

            // Build a context menu for a feature
            map_cm_nodemenu_items = [
                { text: "Informació general", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 10); } } },
                { text: "Escriptori", 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: "Amplieu l'extensió", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 19); } },
                { text: "Allunya fins a l'extensió", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 2); } }
            ];

            // Context menu for clicks other than on feature
            contextmenu_items = [
                { text: "Actualització", callback: function () { refreshMap(true, true); } },
                { text: "Amplia fins a l'extensió", callback: function () { zoomToFitExtent(); } },
                { text: "Mapa del centre aquí", callback: function (obj) { xxmap.mapView.animate({ center: obj.coordinate }); } },
                { text: "Col·loca el node 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 == "Amplieu l'extensió" || item.text == "Allunya fins a l'extensió") { 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>' + "Cerca" + '</label>&nbsp&nbsp<input type=text placeholder="' + "Nom del dispositiu" + '" 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 s'ha trobat cap dispositiu." + '</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', "Seleccioneu un node per col·locar", 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 s'ha trobat cap ubicació." + '<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', "Seguretat del compte", "No es pot accedir a aquesta funció fins que no es verifiqui una adreça electrònica. Això és necessari per a la recuperació de la contrasenya. Aneu a la pestanya \"El meu compte\" per canviar i verificar una adreça de correu electrònic.");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return;
            }

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0)) {
                setModalContent('xxAddAgent', "Seguretat del compte", "No es pot accedir a aquesta funció fins que s'hagi activat l'autenticació de dos factors. Això és necessari per a una seguretat addicional. Aneu a la pestanya \"El meu compte\" i mireu la secció \"Seguretat del compte\".");
                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 dispositiu està connectat"; }
                    if (bat.state == 'dc') { statestr = "El dispositiu funciona amb bateria"; }

                    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>' + "Cap" + '</i>'; }
                if (((meshrights & 4) != 0) && ((!mesh.flags) || ((mesh.flags & 2) == 0 || (mesh.flags & 16)))) { nname = '<span tabindex=0 title="' + "Feu clic aquí per editar el nom del dispositiu del costat 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("Nom", nnameEx); }

                // Attribute: Mesh
                if (mesh) { x += addDeviceAttribute('<span title="' + "El nom del grup de dispositius al qual pertany aquest ordinador." + '">' + "Grup" + '</span>', '<a href=# title="' + "El nom del grup de dispositius al qual pertany aquest ordinador" + '" 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 nom d'aquest ordinador tal com s'estableix al sistema operatiu" + '">' + "Nom del sistema operatiu" + '</span>', '<span title="' + "El nom d'aquest ordinador tal com s'estableix al sistema operatiu" + '">' + 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("Nom d'amfitrió", addLinkConditional((node.host ? EscapeHtml(node.host) : ('<i>' + "Cap" + '</i>')), 'showEditNodeValueDialog(1)', meshrights & 4));
                }

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

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

                // Attribute: Mesh Agent
                if ((node.agent != null) && (node.agent.id != null) && (node.mtype == 3)) {
                    if (node.agent.id == 4) { x += addDeviceAttribute("Tipus de dispositiu", "Windows"); }
                    if (node.agent.id == 6) { x += addDeviceAttribute("Tipus de dispositiu", "Linux"); }
                    if (node.agent.id == 29) { x += addDeviceAttribute("Tipus de dispositiu", "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 += ', ' + "Restringit"; }
                    x += addDeviceAttribute("Agent de malla", str);
                }

                // Attribute: Intel AMT
                if (node.intelamt != null) {
                    var str = '';
                    var provisioningStates = { 0: nobreak("No activat (previ)"), 1: nobreak("No activat (Entrada)"), 2: nobreak("Activat") };
                    if (node.intelamt.ver != null && node.intelamt.state == null) { str += '<i>' + "Estat desconegut" + '</i>, v' + EscapeHtml(node.intelamt.ver); }
                    else if ((node.intelamt.ver == null) && (node.intelamt.state == 2)) { str += '<i>' + "Activat" + '</i>'; }
                    else if ((node.intelamt.ver == null) || (node.intelamt.state == null)) { str += '<i>' + "Versió i estat desconeguts" + '</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à activat en mode de control de client" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { str += ' <span title="' + "Intel&reg; AMT està activat en mode de control d'administració" + '">' + "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à configurat amb seguretat de xarxa 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="' + "Editeu les credencials d'Intel&reg; AMT" + '" onclick=editDeviceAmtSettings("' + node._id + '")>' + "Sense credencials" + '</i>';
                                editUserCredentialsIcon = true;
                            } else {
                                str += ', <i style=color:#FF0000>' + "Sense credencials" + '</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 = "Credencials no vàlides"; }
                            if ((node.intelamt.warn & 8) != 0) { warn = "Prova de credencials"; }
                            if (warn != null) {
                                if ((meshrights & 4) != 0) {
                                    str += ', <i style=color:#FF0000;cursor:pointer title="' + "Editeu les credencials d'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="' + "Editeu les credencials d'Intel&reg; AMT" + '" style=cursor:pointer onclick=editDeviceAmtSettings("' + node._id + '")>';
                        }
                    }

                    var meName = '<span title="' + "Motor de gestió Intel&reg;" + '">' + "Intel&reg; ME" + '<span>';
                    if (typeof node.intelamt.sku == 'number') {
                        if ((node.intelamt.sku & 8) != 0) { meName = '<span title="' + "Tecnologia Intel&reg; de gestió activa" + '">' + "Intel&reg; AMT" + '<span>'; }
                        else if ((node.intelamt.sku & 16) != 0) { meName = '<span title="' + "Gestió estàndard Intel&reg;" + '">' + "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 d'agent", 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 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 operatiu", 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>' + "D'acord" + '</span>'); } else { y.push("AV" + ' - <span style=color:red>' + "DOLENT" + '</span>'); } }
                    if (node.wsc.autoUpdate != null) { if (node.wsc.autoUpdate == 'OK') { y.push("Actualització" + ' - <span style=color:green>' + "D'acord" + '</span>'); } else { y.push("Actualització" + ' - <span style=color:red>' + "DOLENT" + '</span>'); } }
                    if (node.wsc.firewall != null) { if (node.wsc.firewall == 'OK') { y.push("Tallafoc" + ' - <span style=color:green>' + "D'acord" + '</span>'); } else { y.push("Tallafoc" + ' - <span style=color:red>' + "DOLENT" + '</span>'); } }
                    x += addDeviceAttribute("Seguretat 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("Protecció en temps real" + ' - <span style=color:green>' + "Encès" + '</span>'); } else { y.push("Protecció en temps real" + ' - <span style=color:red>' + "Apagat" + '</span>'); } }
                    if (node.defender.TamperProtected != null) { if (node.defender.TamperProtected == true) { y.push("Protecció contra manipulacions" + ' - <span style=color:green>' + "Encès" + '</span>'); } else { y.push("Protecció contra manipulacions" + ' - <span style=color:red>' + "Apagat" + '</span>'); } }
                    if (node.defender.AntivirusSignatureVersion != null) { y.push("Versió de signatura" + ' - <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>' + "D'acord" + '</span>'); } else { y.push("AV" + ' - <span style=color:red>' + "DOLENT" + '</span>'); } }
                    if (node.lsc.firewall != null) { if (node.lsc.firewall == 'OK') { y.push("Tallafoc" + ' - <span style=color:green>' + "D'acord" + '</span>'); } else { y.push("Tallafoc" + ' - <span style=color:red>' + "DOLENT" + '</span>'); } }
                    if (y.length > 0) x += addDeviceAttribute("Seguretat 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>' + "Inhabilitat" + '</span>'; }
                            if (node.av[i].updated !== true) { avx += ' - <span style=color:red>' + "Obsolet" + '</span>'; }
                            if ((node.av[i].enabled == true) && (node.av[i].updated == true)) { avx += ' - <span style=color:green>' + "D'acord" + '</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 + ' - ' + "Tancat" : "Tancat", true); }
                        if (label) { return '<span style=cursor:default title=\'' + label + '\'>' + escapedUser + '</span>'; }
                        return escapedUser;
                    }).join(', ');
                    x += addDeviceAttribute((node.users.length > 1 ? "Usuaris actius" : "Usuari actiu"), u);
                }

                // Windows Idle Time
                if (node.idletime && node.idletime != -1) { x += addDeviceAttribute("Temps d'inactivitat", 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("Petició de l'escriptori+barra d'eines"); } else if (consent & 0x0040) { meshFeatures.push("Barra d'eines d'escriptori"); } else if (consent & 0x0008) { meshFeatures.push("Petició d'escriptori"); } else { if (consent & 0x0001) { meshFeatures.push("Notificació d'escriptori"); } }
                    if (consent & 0x0010) { meshFeatures.push("Indicador del terminal"); } else { if (consent & 0x0002) { meshFeatures.push("Notificació de terminal"); } }
                    if (consent & 0x0020) { meshFeatures.push("Solicitud de fitxers"); } else { if (consent & 0x0004) { meshFeatures.push("Notificació de fitxers"); } }
                    if (consent == 7) { meshFeatures = ["Notificar sempre"]; }
                    if ((consent & 56) == 56) { meshFeatures = ["Sempre prompte"]; }

                    meshFeatures = meshFeatures.join(', ');
                    if (meshFeatures == '') { meshFeatures = '<i>' + "Cap" + '</i>'; }
                    x += addDeviceAttribute("Consentiment de l'usuari", 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("Connecta't"); }
                    if (devNotify & 4) { devNotifyStr.push("Desconnecta"); }
                    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("Correu electrònic ({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("Missatgeria ({0})", xx)); }
                    }
                }
                devNotifyStr = devNotifyStr.join(', ');
                if (devNotifyStr == '') { devNotifyStr = '<i>' + "Cap" + '</i>'; }
                x += addDeviceAttribute("Notificacions", 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="' + "L'agent de malla està connectat i llest per al seu ús." + '">' + "Agent de malla" + '</span>');
                    if ((node.conn & 2) != 0) cstate.push('<span title="' + "Intel&reg; AMT CIRA està connectat i llest per al seu ús." + '">' + "Intel&reg; AMT CIRA" + '</span>');
                    else if ((node.conn & 4) != 0) cstate.push('<span title="' + "Intel&reg; AMT és encaminable i llest per al seu ús." + '">' + "Intel&reg; AMT" + '</span>');
                    if ((node.conn & 8) != 0) cstate.push('<span title="' + "L'agent de malla és accessible mitjançant un altre agent com a relé." + '">' + "Relleu de malla" + '</span>');
                    if ((node.conn & 16) != 0) { cstate.push('<span title="' + "La connexió MQTT al dispositiu està activa." + '">' + "MQTT" + '</span>'); }
                    x += addDeviceAttribute("Connectivitat", cstate.join(', '));
                }

                // Node tags
                var groupingTags = '<i>' + "Cap" + '</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("Etiquetes", '<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("Etiquetes", '<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-Usuari+Passa" : ((node.ssh == 2) ? "SSH-Usuari+Clau+Passa" : "SSH-Usuari+Clau")) + ' <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-Usuari+Passa" : ((node.ssh == 2) ? "SSH-Usuari+Clau+Passa" : "SSH-Usuari+Clau"))); }
                        if (node.rdp != null) { y.push("RDP"); }
                    }
                    x += addDeviceAttribute("Credencials", 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="' + "Grups de dispositius per als quals aquest dispositiu és un relé" + '">' + "Relleu per" + '</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="' + "Accions" + '" title="' + "Realitzeu accions d'alimentació al dispositiu" + '" onclick=deviceActionFunction() />'; }
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Notes" + '" title="' + "Consulta les notes sobre aquest dispositiu" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Registra l'esdeveniment" + '" title="' + "Escriu un esdeveniment per a aquest dispositiu" + '" 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="' + "Correr" + '" title="' + "Executeu ordres en aquest dispositiu" + '" 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="' + "Missatge" + '" title="' + "Mostra un missatge de text al dispositiu remot" + '" 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="' + "Xatejar" + '" title="' + "Obre la finestra de xat en aquest ordinador" + '" 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="' + "Crea un enllaç per compartir aquest dispositiu amb un convidat" + '" 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="' + "Tanca" + '" title="' + "Tanca" + '" 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="' + "Encendre" + '" title="' + "Encendre" + '" onclick=setIpPduState(1) />'; }
                        }
                    } else {
                        if (meshrights & 8) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Control remot" + '" title="' + "Control remot" + '" 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="' + "Mou aquest dispositiu a un altre grup de dispositius" + '">' + "Canvia de grup" + '</a>';
                }
                if ((meshrights & 0x8000) != 0) { // MESHRIGHT_UNINSTALL
                    x += '&nbsp;<a href=# onclick=p10showDeleteNodeDialog("' + node._id + '") title="' + "Elimina aquest dispositiu" + '">' + "Suprimeix el dispositiu" + '</a>';
                }
                x += '</div><div class="p10html3left">';
                if ((node.agent) && (node.mtype != 3) && ((meshrights & 1048576) != 0)) x += '<a href=# onclick=p10showNodeNetInfoDialog("' + node._id + '") title="' + "Mostra la informació de la interfície de xarxa del dispositiu" + '">' + "Interfícies" + '</a>&nbsp;';
                if ((features & 0x00008000) && (xxmap != null)) x += '<a href=# onclick=p10showNodeLocationDialog("' + node._id + '") title="' + "Mostra la informació d'ubicacions del dispositiu" + '">' + "Ubicació" + '</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="' + "Encaminador de trànsit utilitzat per connectar-se a un dispositiu mitjançant aquest 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="' + "Obriu el 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="' + "Requereix la instal·lació del router MeshCentral" + '.">' + "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="' + "Requereix la instal·lació del router MeshCentral." + '">' + "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="' + "Requereix la instal·lació del router MeshCentral." + '">' + "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="' + "Requereix la instal·lació del router MeshCentral." + '">' + 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="' + "Inicieu la sessió VNC basada en web a aquest dispositiu" + '.">' + "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="' + "Inicieu la sessió RDP basada en web a aquest dispositiu" + '.">' + "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="' + "Inicieu la sessió SSH basada en web en aquest dispositiu" + '.">' + "Web-SSH" + '</a>&nbsp;';
                    }

                    // MQTT options
                    if ((meshrights == 0xFFFFFFFF) && (features & 0x00400000)) { x += '<a href=# onclick=p10showMqttLoginDialog("' + node._id + '") title="' + "Obteniu les credencials d'inici de sessió MQTT per a aquest dispositiu." + '">' + "Inici de sessió 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="' + "L'agent s'està executant en un dispositiu remot amb privilegis reduïts." + '">' + "Restringit" + '</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="' + "Port del commutador connectat" + '">' + "Port del commutador connectat" + '</span>' + agentPrivilages;
                        } else {
                            powerstate += '<span style=font-size:12px title="' + "Port IP-KVM connectat" + '">' + "Port IP-KVM connectat" + '</span>' + agentPrivilages;
                        }
                    } else {
                        powerstate += '<span style=font-size:12px title="' + "Agent connectat" + '">' + "Agent connectat" + '</span>' + agentPrivilages;
                    }
                }
                if ((connectivity & 2) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT connectat" + '">' + "Intel&reg; AMT connectat" + '</span>'; }
                else if ((connectivity & 4) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT detectat" + '">' + "Intel&reg; AMT detectat" + '</span>'; }
                if ((connectivity & 16) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "MQTT connectat" + '">' + "Canal MQTT connectat" + '</span>'; }
                if ((powerstate == '') && node.lastconnect) { powerstate = '<span style=font-size:12px>' + "Vist per últim cop:" + '<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> ' + "Afegeix usuari" + '</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> ' + "Afegeix un grup d'usuaris" + '</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>' + "Autoritzacions d'usuari" + '</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="' + "Elimina els drets del grup d'usuaris a aquest grup de dispositius" + '" 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="' + "Elimina els drets d'usuari d'aquest grup de dispositius" + '" 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 ? "Grup d'usuaris" : "Usuari") + '" 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>' + "Cap usuari amb permisos especials de dispositiu" + '</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>' + "Compartició activa de dispositius" + '</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="' + "Enllaç per compartir dispositiu" + '" 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="' + "Elimina l'ús compartit de dispositius" + '" class="fa-fw fa-solid fa-trash text-danger"></i>';
                        var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Escriptori", "Escriptori + Terminal", "Fitxers", "Terminal + Fitxers", "Escriptori + Fitxers", "Escriptori + Terminal + Fitxers"][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} durant {2} minut", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            } else {
                                details = format("{0}, {1} durant {2} minuts", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            }
                        }
                        if (dshare.recurring == 1) { details += ", Recurrent diàriament"; }
                        if (dshare.recurring == 2) { details += ", Recurrent setmanal"; }
                        if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Visualitza només l'escriptori"; }
                        if (dshare.consent != null) {
                            if (dshare.consent == 0) { details += ", Sense consentiment"; } else {
                                if ((dshare.consent & 0x0038) != 0) { details += ", Sol·licitar el consentiment"; }
                                if ((dshare.consent & 0x0040) != 0) { details += ", Barra d'eines"; }
                            }
                        }
                        var guestName = EscapeHtml(dshare.guestName);
                        if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Autocompartició de l'agent" + '</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', "Funcionament elèctric", 'Perform power off?');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: 2 }); });
            } else {
                // Turn on
                setModalContent('xxAddAgent', "Funcionament elèctric", '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">' + "Notificacions de pàgines web" + '</div>';
            x += '<div><label><input id=p20notifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Connexions de dispositius" + '</label></div>';
            x += '<div><label><input id=p20notifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Desconnexions del dispositiu" + '</label></div>';
            if (currentNode.intelamt != null) { fx += 2; x += '<div><label><input id=p20notifyIntelAmtKvmActions type=checkbox class="form-check-input me-2" />' + "Esdeveniments d'escriptori i sèrie Intel&reg; AMT" + '</label></div>'; }
            if (fx & 1) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificacions per correu electrònic" + '</div>';
                x += '<div><label><input id=p20enotifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Connexions de dispositius" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Desconnexions del dispositiu" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceHelp type=checkbox class="form-check-input me-2" />' + "Sol·licituds d'ajuda" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificacions de missatgeria" + '</div>';
                x += '<div><label><input id=p20emsgDeviceConnect type=checkbox class="form-check-input me-2" />' + "Connexions de dispositius" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Desconnexions del dispositiu" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceHelp type=checkbox class="form-check-input me-2" />' + "Sol·licituds d'ajuda" + '</label></div>';
            }
            setModalContent('xxAddAgent', "Configuració de notificacions", 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 "Drets complets del dispositiu"; }
            var str = [];
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Sense entrada");
                if (rights & 512) str1.push("Sense terminal");
                if (rights & 1024) str1.push("No hi ha fitxers");
                if (rights & 2048) str1.push("No AMT");
                if (rights & 4096) str1.push("Entrada limitada");
                if (rights & 65536) str1.push("Sense escriptori");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Compartició de convidats");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Control"); }
            }
            if (rights & 16) str.push("Consola");
            if (rights & 32) str.push("Fitxers del servidor");
            if (rights & 64) str.push("Desperta");
            if (rights & 128) str.push("Notes");
            if (rights & 8192) str.push("Limitar esdeveniments");
            if (rights & 16384) str.push("Xatejar");
            if (rights & 32768) str.push("Desinstal·la");
            if (rights & 131072) str.push("Ordres");
            if (rights & 262144) str.push("Restablir/Desactivar");
            if (rights & 524288) str.push("Compartint");
            if (rights & 1048576) str.push("Detalls");
            if (rights & 2097152) str.push("Relleu");
            if (str.length == 0) return "Sense Drets";
            return str.join(', ');
        }

        function makeDeviceGroupRightsString(rights) {
            if (rights == 0xFFFFFFFF) { return "Drets plens"; }
            var str = [];
            if (rights & 1) str.push("Edita el grup");
            if (rights & 2) str.push("Gestionar Usuaris");
            if (rights & 4) str.push("Gestiona els dispositius");
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Sense entrada");
                if (rights & 512) str1.push("Sense terminal");
                if (rights & 1024) str1.push("No hi ha fitxers");
                if (rights & 2048) str1.push("No AMT");
                if (rights & 4096) str1.push("Entrada limitada");
                if (rights & 65536) str1.push("Sense escriptori");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Compartició de convidats");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Control"); }
            }
            if (rights & 16) str.push("Consola");
            if (rights & 32) str.push("Fitxers del servidor");
            if (rights & 64) str.push("Desperta");
            if (rights & 128) str.push("Notes");
            if (rights & 8192) str.push("Limitar esdeveniments");
            if (rights & 16384) str.push("Xatejar");
            if (rights & 32768) str.push("Desinstal·la");
            if (rights & 131072) str.push("Ordres");
            if (rights & 262144) str.push("Restablir/Desactivar");
            if (rights & 524288) str.push("Compartint");
            if (rights & 1048576) str.push("Detalls");
            if (rights & 2097152) str.push("Relleu");
            if (str.length == 0) return "Sense Drets";
            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', "Afegeix un esdeveniment del dispositiu", '<textarea id=d2devEvent style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea><span style=font-size:10px>' + "Això afegirà una entrada al registre d'esdeveniments d'aquest dispositiu." + '<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>' + "Altres administradors de grups de dispositius poden veure i canviar les notes del grup de dispositius." + '<span>'; }
            if (showNotesPanel === 'true') { x += ' <span style=font-size:10px><a target=_blank href=\'https://www.markdownguide.org/cheat-sheet/\'>' + "Sintaxi Markdown admesa" + '</a></span>'; }
            setModalContent('xxAddAgent', "Notes", 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', "Obriu la pàgina al dispositiu", '<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>' + "Mostra un quadre de missatge al dispositiu remot." + '</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>' + "Mostra durant 2 minuts (per defecte)" + '</option>';
            x += '<option value=10>' + "Mostra durant 10 minuts" + '</option>';
            x += '<option value=30>' + "Mostra durant 30 minuts" + '</option>';
            x += '<option value=60>' + "Mostra durant 60 minuts" + '</option>';
            x += '<option value=0>' + "Mostra el missatge fins que l'usuari l'elimini" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Missatge del dispositiu", 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', "Bloqueig d'entrada remota", (value == 1) ? "Bloquejar el ratolí i el teclat de l'usuari remot?" : "Vols desbloquejar el ratolí i el teclat de l'usuari remot?");
            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>' + "Teclat 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',         "Sortir de pantalla completa",                          'deskToggleFull();deskCloseMobileActions()',                       'text-danger') +
                monitorRow +
                row(cls, (_mobileZoomMode==='native' ? 'fa-compress-arrows-alt' : 'fa-expand-arrows-alt'),
                    (_mobileZoomMode==='native' ? "Escala per ajustar" : "Mida nativa (desplaçable)"),  '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',          "Teclat 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 ? "Desactiva el mode de trackpad" : "Activa el mode de trackpad"),  'deskToggleTrackpad();deskCloseMobileActions()') +
                row(cls, 'fa-hand-pointer',      (swap ? "Desactiva el mode de clic dret" : "Activa el mode de clic dret"),   'deskToggleRightClickMode();deskCloseMobileActions()') +
                row(cls, 'fa-font',              "Escriu text",                                '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',           "Dreceres de teclat",                       'deskCustomizeKeys();deskCloseMobileActions()') +
                row(cls, 'fa-paste',             "Porta-retalls",                               'showDeskClip();deskCloseMobileActions()') +
                row(cls, 'fa-clipboard',         "Carrega el porta-retalls",                         'deskClipboardOutFunction();deskCloseMobileActions()') +
                row(cls, 'fa-rotate-right',      "Gira a la dreta",                             'drotate(1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate-left',       "Gira a l'esquerra",                              'drotate(-1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate',            "Actualitza l'escriptori",                          'deskRefreshFunction();deskCloseMobileActions()') +
                row(cls, 'fa-gear',              "Configuració de l'escriptori",                         'showDesktopSettings();deskCloseMobileActions()') +
                row(end, 'fa-plug-circle-xmark', "Desconnecta",                               '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ó de brindis" + '</option><option value=1>' + "Caixa de missatges" + '</option><option value=3>' + "Caixa d'alerta" + '</option></select>';
            x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Títol" + '" 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>' + "Mostra durant 2 minuts (per defecte)" + '</option>';
            x += '<option value=10>' + "Mostra durant 10 minuts" + '</option>';
            x += '<option value=30>' + "Mostra durant 30 minuts" + '</option>';
            x += '<option value=60>' + "Mostra durant 60 minuts" + '</option>';
            x += '<option value=0>' + "Mostra el missatge fins que l'usuari l'elimini" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Notificació del dispositiu", 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', "Bloqueja l'escriptori", "Bloquejar l'escriptori de l'usuari?");
                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 enllaç que permet a un convidat sense compte controlar aquest dispositiu durant un temps limitat." + '<br /><br />';
            x += addHtmlFormFloating("Nom del convidat", '<input id=d2inviteName class="form-control" maxlength=128 type=text onkeyup=showShareDeviceValidate() />');
            var deskFull = '<option value=2>' + "Escriptori" + '</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>' + "Fitxers" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x400) != 0)) { fullFiles = ''; }
            var deskFiles = '<option value=5>' + "Escriptori + Fitxers" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x500) != 0)) { deskFiles = ''; }
            var termFiles = '<option value=6>' + "Terminal + Fitxers" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x600) != 0)) { termFiles = ''; }
            var allFeatures = '<option value=7>' + "Escriptori + Terminal + Fitxers" + '</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>' + "Escriptori, només visualització" + '</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("Tipus", '<select id=d2shareType class="form-select" onchange=showShareDeviceValidate()>' + y + '</select>');
            var options = { 1: "1 minut", 5: "5 minuts", 10: "10 minuts", 15: "15 minuts", 30: "30 minuts", 45: "45 minuts", 60: "60 minuts", 120: "2 hores", 240: "4 hores", 480: "8 hores", 720: "12 hores", 960: "16 hores", 1440: "24 hores", 2880: "2 dies", 5760: "4 dies", 0: "Il·limitat" }
            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("Validesa", '<select id=d2timeRange class="form-select" onchange=showShareDeviceValidate()><option value=0>' + "Començant ara" + '</option><option value=1>' + "Interval de temps" + '</option><option value=2>' + "Recurrent diàriament" + '</option><option value=3>' + "Recurrent setmanal" + '</option></select>');
            x += '<div id=d2modenow>';
            x += addHtmlFormFloating("Temps d'expiració", '<select id=d2inviteExpire class="form-select">' + y + '</select>');
            x += '</div><div id=d2moderange style=display:none>';
            x += addHtmlFormFloating("Interval de temps d'inici", '<input id=d2timeRangeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Seleccioneu la data i l'hora..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Final de l'interval de temps", '<input id=d2timeRangeEndSelector class="flatpickr form-select" type="text" placeholder="' + "Seleccioneu la data i l'hora..." + '" data-id="altinput">');
            x += '</div><div id=d2moderecurring style=display:none>';
            x += addHtmlFormFloating("L'hora d'inici", '<input id=d2timeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Seleccioneu la data i l'hora..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Durada", '<select id=d2inviteDuration class="form-select">' + z + '</select>');
            x += '</div>';
            if (currentNode.agent.caps & 1) { x += '<div id=d2userConsentSelector>' + addHtmlFormFloating("Consentiment de l'usuari", '<select id=d2userConsent class="form-select"><option value=0>' + "Notificar només" + '<option value=1>' + "Sol·licitud de consentiment" + '</option><option value=2>' + "Sense consentiment" + '</option></select>') + '</div>'; }
            x += '<div id=d2httpPortSelector>' + addHtmlFormFloating("Port", '<input id=d2httpPort class="form-control" value=80 onkeyup=showShareDeviceValidate()></input>') + '</div>';
            x += '<div id=d2httpsPortSelector>' + addHtmlFormFloating("Port", '<input id=d2httpsPort class="form-control" value=443 onkeyup=showShareDeviceValidate()></input>') + '</div>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Comparteix el dispositiu", 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 = "Seleccioneu una operació per realitzar en aquest dispositiu." + '<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>' + "Flash" + '</option>';
                    y += '<option value=401>' + "Vibrar" + '</option>';
                    z += '<div id=d2devicetimediv>' + addHtmlFormFloating("Temps", '<select id=d2devicetime class=form-select><option value=1000>' + "1 segon" + '</option><option value=5000>' + "5 segons" + '</option><option value=10000>' + "10 segons" + '</option></select>') + '</div>';
                }
            } else {
                if ((rights & 64) != 0) { count++; y += '<option value=100>' + "Desperta" + '</option>'; } // Wake-up permission
                if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Executeu ordres" + '</option>'; } // Remote command permission
                if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Dormir" + '</option><option value=3>' + "Restableix" + '</option><option value=2>' + "Apagar" + '</option>'; }
                if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Envia un missatge MQTT" + '</option>'; }
                if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
                    count++;
                    y += '<option value=310>' + "Restabliment Intel&reg; AMT" + '</option>';
                    y += '<option value=308>' + "Intel&reg; AMT Apagat" + '</option>';
                    if ((xxcurrentView == 11) || (xxcurrentView == 12)) { // Only show these options on terminal or desktop tabs
                        y += '<option value=312>' + "Intel&reg; AMT Restableix a la BIOS" + '</option>';
                        y += '<option value=311>' + "Enceneu Intel&reg; AMT a la 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>' + "Encès 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>' + "Intel&reg; AMT One Click Recovery" + '</option>'; } // CIRA (2) or AMT (4) connected
                if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Desinstal·leu l'agent" + '</option>'; }
            }
            y += '</select>';
            x += addHtmlFormFloating("Funcionament", y);
            if (count == 0) { x = "Actualment no hi ha cap acció disponible per a aquest dispositiu."; }
            xxdialogButtons = (count == 0) ? 2 : 3; xxdialogTag = x + z;
            setModalContent('xxAddAgent', "Acció del dispositiu", 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 = "Executeu ordres als dispositius seleccionats." + '<br />';
                    if (wintype == true) {
                        x += '<select id=d2cmdtype style=width:100%;margin-bottom:4px;margin-top:4px> class=form-select>';
                        x += '<option value=1>' + "Símbol del sistema de comandes de Windows" + '</option><option value=2>' + "Windows PowerShell" + '</option>';
                        if (linuxtype == true) { x += '<option value=3>' + "Linux/BSD/macOS Command Shell" + '</option>'; }
                        x += '</select>';
                    }
                    x += '<select id=d2cmduser style=width:100%;margin-bottom:4px><option value=0>' + "Executar com a agent" + '</option><option value=1>' + "Executeu com a usuari, agent si no hi ha usuari" + '</option><option value=2>' + "S'ha d'executar com a usuari" + '</option></select>';
                    x += '<textarea id=d2runcmd style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea>';
                    setModalContent('xxAddAgent', "Executeu ordres", 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', "Intel&reg; AMT One Click Recovery", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deviceActionOneClickRecovery());
                d3init();
            } else if (op == 302) { // Intel AMT power on
                setModalContent('xxAddAgent', "Funcionament d'energia Intel&reg; AMT", "Engegueu 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', "Funcionament d'energia Intel&reg; AMT", "Vols apagar Intel&reg; AMT?<br><br><b>NOTA: si hi ha una sessió AMT activa, l'ordre d'apagada serà rebutjada, de manera que primer has de desconnectar-te de la sessió AMT!</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', "Funcionament d'energia Intel&reg; AMT", "Vols fer el restabliment d'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', "Funcionament d'energia Intel&reg; AMT", "Engegueu Intel&reg; AMT a la 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', "Funcionament d'energia Intel&reg; AMT", "Vols fer el restabliment d'Intel&reg; AMT a la 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>' + "Dia" + '</th><th scope=col>' + "Estat de potència de 7 dies" + '<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="' + "Baixeu esdeveniments de poder" + '" 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("Nom d'usuari", '<input id=dp10username class="form-control" maxlength=32 autocomplete=nope placeholder="admin" onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            x += addHtmlFormFloating("Contrasenya", '<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("Seguretat", '<select id=dp10tls class="form-select"><option value=0>' + "Sense seguretat TLS" + '</option><option value=1>' + "Es requereix seguretat TLS" + '</option></select>'); }
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { buttons = 7; }
            setModalContent('xxAddAgent', "Editeu les credencials d'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("Missatge", '<textarea id=dp2msg class="form-control" maxlength=4096 style=width:100%;height:150px;resize:none onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1)></textarea>');
            setModalContent('xxAddAgent', "Envia un missatge 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("Esteu segur que voleu desinstal·lar els agents {0} seleccionats?", nodeids.length); } else { x = "Esteu segur que voleu desinstal·lar l'agent seleccionat?"; }
            x += '<br /><br />';
            if (nodeids.length > 1) { x += "Això no eliminarà els dispositius del servidor, però els dispositius ja no es podran connectar al servidor. Es perdrà tot l'accés remot als dispositius. Els dispositius han d'estar connectats perquè aquesta ordre funcioni."; } else { x += "Això no eliminarà aquest dispositiu del servidor, però el dispositiu ja no es podrà connectar al servidor. Es perdrà tot l'accés remot al dispositiu. El dispositiu ha d'estar connectat perquè aquesta ordre funcioni."; }
            x += '<br /><br /><label style=color:red><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Confirmeu" + '</label>';
            setModalContent('xxAddAgent', "Desinstal·la l'agent", 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) ? ("Seleccioneu un grup nou per a aquest dispositiu" + '<br /><br />') : ("Seleccioneu un grup nou per als dispositius seleccionats" + '<br /><br />');
                x += addHtmlFormFloating("Nou grup de dispositius", y);
                setModalContent('xxAddAgent', "Canvia de grup", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showChangeGroupDialogEx(3, nodeids));
            } else {
                setModalContent('xxAddAgent', "Canvia de grup", '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("Esteu segur que voleu suprimir el node {0}?", EscapeHtml(currentNode.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Confirmeu" + '</label>';
            xxdialogButtons = 3; xxdialogTag = nodeid;
            setModalContent('xxAddAgent', "Suprimeix el node", 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ó del dispositiu", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            return false;
        }

        // Show network interfaces
        function p10showNodeNetInfoDialog() {
            if (xxdialogMode) return false;
            xxdialogButtons = 1; xxdialogTag = 'if' + currentNode._id;
            setModalContent('xxAddAgent', "Interfícies de xarxa", '<div id=d2netinfo>' + "Carregant..." + '</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 és una eina de Windows per a l'assignació de ports TCP. Podeu, per exemple, introduir RDP a un dispositiu remot mitjançant aquest 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">' + "Executeu MeshCentral Router i feu clic a \"instal·la\" perquè es pugui iniciar des del navegador." + '</div>';
            x += '<br /><a style=cursor:pointer target="mcrouterframe" rel="noreferrer noopener" href="' + url + '"><input type=button style=width:100%;cursor:pointer value="' + "Inicieu l'encaminador MeshCentral" + '" class="btn btn-primary btn-block" /></a>';
            x += '<iframe class="d-none" name="mcrouterframe"></iframe>';
            setModalContent('xxAddAgent', "Encaminador 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 (64 bits)" + '</option>';
            y += '<option value=3>' + "Windows x86 (32 bits)" + '</option>';
            y += '<option value=43>' + "Windows ARM (64 bits)" + '</option>';
            y += '<option value=6>' + "Linux x86 (64 bits)" + '</option>';
            y += '<option value=5>' + "Linux x86 (32 bits)" + '</option>';
            y += '<option value=16>' + "macOS x86 (64 bits)" + '</option>';
            y += '<option value=29>' + "macOS ARM (64 bits)" + '</option>';
            y += '<option value=25>' + "Linux ARM, Raspberry Pi (32 bits)" + '</option>';
            y += '<option value=26>' + "Linux ARM, Raspberry Pi (64 bits)" + '</option>';
            y += '</select>';

            var x = '';
            if (mode == 0) { x += '<div>' + "MeshCmd és una eina de línia d'ordres que realitza moltes operacions diferents. El fitxer d'acció es pot descarregar i editar opcionalment per proporcionar informació i credencials del servidor." + '<br /><br />'; }
            if (mode == 1) { x += '<div>' + "Baixeu \"meshcmd\" amb un fitxer d'acció per dirigir el trànsit a través d'aquest servidor a aquest dispositiu. Assegureu-vos d'editar meshaction.txt i afegir la contrasenya del vostre compte o fer els canvis necessaris." + '<br /><br />'; }
            x += addHtmlFormFloating("Sistema operatiu", 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("Fitxer d'acció", '<a onclick=downloadFile("meshagents?meshaction=generic' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary>' + "MeshAction (.txt)" + '</a>'); }
            if (mode == 1) { x += addHtmlValue("Fitxer d'acció", '<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', "Descarrega 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 (executable Win x86-32)"; }
            if (os == 4) { osn = "MeshCmd (executable Win x86-64)"; }
            if (os == 43) { osn = "MeshCmd (executable Win ARM-64)"; }
            if (os == 5) { osn = "MeshCmd (Linux x86, 32 bits)"; }
            if (os == 6) { osn = "MeshCmd (Linux x86, 64 bits)"; }
            if (os == 16) { osn = "MeshCmd (macOS, x86-64bit)"; }
            if (os == 29) { osn = "MeshCmd (macOS, ARM-64bit)"; }
            if (os == 25) { osn = "MeshCmd (Linux ARM, 32 bits)"; }
            if (os == 26) { osn = "MeshCmd (Linux ARM, 64 bits)"; }
            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ó d'icones", 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', "Edita el dispositiu", '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', "Edita el dispositiu", '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 = ["Nom del dispositiu", "Nom d'amfitrió", "Descripció", "Etiquetes"];
        var showEditNodeValueDialog_modes2 = ['name', 'host', 'desc', 'tags'];
        var showEditNodeValueDialog_modes3 = ['', '', '', "Etiqueta 1, etiqueta 2, etiqueta 3"];
        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', "Edita el dispositiu", 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("Port RDP {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 = ['', "S'està esperant que l'usuari concedeixi l'accés...", "Negat", "No s'ha pogut iniciar la sessió del terminal remot, {0} ({1})", "Temps mort", "S'han rebut dades de xarxa no vàlides", "No es pot capturar la pantalla", "La negociació del protocol ha fallat ({0})", "NLA no compatible", "SSL requerit pel servidor", "SSL no permesa pel servidor", "Certificat SSL no al servidor", "Banderes inconsistents", "Híbrid requerit pel servidor", "SSL amb autenticació d'usuari requerida pel 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("DataChannel", {}); // { 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("Credencials", '<select id=d2mode class="form-select" onchange=askRdpCredentialsValidate()><option value=1>' + "Utilitzeu les credencials del servidor" + '</option><option value=2>' + "Utilitzeu credencials noves" + '</option></select>');
                x += '<div id=d2cred style=display:none>';
            } else {
                x += '<div id=d2cred>';
            }
            x += addHtmlFormFloating("Domini", '<input type=text id=d2domain class="form-control" maxlength=128 />');
            x += addHtmlFormFloating("Nom d'usuari", '<input type=text id=d2user class="form-control" onKeyUp=askRdpCredentialsValidate() maxlength=128 />');
            x += addHtmlFormFloating("Contrasenya", '<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>' + "Recordeu les credencials" + '</label>'); }
            x += '</div>';
            x += MoreStart();
            x += addHtmlFormFloating("Alt Shell", '<input type=text id=d2altshell class="form-control" maxlength=2048 />');
            x += addHtmlFormFloating("Director de treball", '<input type=text id=d2workdir class="form-control" maxlength=2048 />');
            x += MoreEnd();
            setModalContent('xxAddAgent', "Credencials 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} mirant", 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("L'hora d'inici", 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 connexió" : format("{0} connexions", conn.metadata.users[i]);
                        x += addHtmlValue2(getUserName(i), val);
                    }
                }
                xxdialogTag = 'sessionMetadata' + cid;
                setModalContent('xxAddAgent', "Informació de la sessió", 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 = "Tot Focus";
                    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', "Configuració de l'escriptori remot", 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 = "Tot Focus"; }
                    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 = ["Tot Focus", "Petit focus", "Gran focus"][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: "Espai enrere", 9: "Tab", 13: "Entra", 27: "escapar", 32: "Espai", 44: "Imprimir pantalla", 45: "Insereix", 46: "Del", 36: "a casa", 35: "Final", 33: "Amunt pàgina", 34: "Avançar pagina", 37: "Esquerra", 38: "Amunt", 39: "Dret", 40: "Avall", 0: "Cap" }

        function keyShortcutTotext(n) {
            var x = [];
            if (n & 0x010000) { x.push("Canvi"); }
            if (n & 0x020000) { x.push("Alt"); }
            if (n & 0x080000) { x.push("Ctrl"); }
            if (n & 0x100000) { x.push("Guanyar"); }
            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', "Dreceres de teclat", 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("Nom", '<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=' + "Afegeix" + ' onclick=addDeskCustomizeString() class="form-control" />');
            x + '</div>';
            setModalContent('xxAddAgent', "Personalització de les cadenes del teclat", 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 s'han definit cadenes de teclat" + '</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>' + "Introduïu text i feu clic a D'acord per escriure'l de manera remota. Assegureu-vos de col·locar el cursor remot a la posició correcta abans 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 de teclat remota", 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="' + "Obteniu el porta-retalls" + '" onclick=showDeskClipGet()></div>';
            if ((features2 & 0x1000) == 0) x += '<div class="col"><input class="btn btn-secondary" id=dlgClipSet type=button value="' + "Estableix el porta-retalls" + '" 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 porta-retalls remot és vàlid durant 60 segons." + '</span></div></div></div>';
            x += '</div>'
            xxdialogTag = 'clipboard';
            setModalContent('xxAddAgent', "Porta-retalls remot", 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="' + "Atura el procés" + '" 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("Detalls del procés, #{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 = "Parat"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Córrer"; }
                        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("Nom", service.name); }
                    if (service.displayName) { x += addHtmlValue("Nom de visualització", service.displayName); }
                    if (service.status) {
                        var ss = capitalizeFirstLetter(service.status.state.toLowerCase());
                        if (ss == 'Stopped') { ss = "Parat"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Córrer"; }
                        if (service.status.state) { x += addHtmlValue("Estat", 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("Interactiu"); }
                        if (service.status.isKernelDriver === true) { serviceTypes.push("KernelDriver"); }
                        if (service.status.isOwnProcess === true) { serviceTypes.push("OwnProcess"); }
                        if (service.status.isSharedProcess === true) { serviceTypes.push("Procés compartit"); }
                        if (serviceTypes.length > 0) { x += addHtmlValue("Tipus", serviceTypes.join(', ')); }
                    }
                    if (service.startType) { x += addHtmlValue("Tipus d'inici", service.startType); }
                    if (service.user) { x += addHtmlValue("Usuari", service.user); }
                    if (service.installedBy) { x += addHtmlValue("Instal·lat per", service.installedBy); }
                    if (service.installedDate) { x += addHtmlValue("Data d'instal·lació", 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("Reinicieu el recompte d'errors després", abc + ' Day' + (abc != 1 ? 's' : ''));
                        }
                        if (service.failureActions.actions) {
                            if (service.failureActions.actions[0]) { x += addHtmlValue("Primer fracàs", service.failureActions.actions[0].type); }
                            if (service.failureActions.actions[1]) { x += addHtmlValue("Segon fracàs", service.failureActions.actions[1].type); }
                            if (service.failureActions.actions[2]) { x += addHtmlValue("Falles posteriors", service.failureActions.actions[2].type); }
                        }
                    }
                    x += '<br/><div style=float:right;margin-bottom:12px><input type=button value="' + "Tanca" + '" onclick=showServiceDetailsDialogEx(0,' + index + ')></div><div style=margin-bottom:12px><input type=button value="' + "Començar" + '" onclick=showServiceDetailsDialogEx(1,' + index + ')><input type=button value="' + "Atura" + '" onclick=showServiceDetailsDialogEx(2,' + index + ')><input type=button value="' + "Reinicia" + '" onclick=showServiceDetailsDialogEx(3,' + index + ')></div>';
                    xxdialogTag = "servei_" + index;
                    setModalContent('xxAddAgent', "Detalls del servei", 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 = "servei_" + index;
                setModalContent('xxAddAgent', "Detalls del servei", '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 = "DesktopSession" + '-' + 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 = "Escriptori" + '-' + 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 = "Totes les pantalles"; allDisplays = 2; } // Language translation
                if (str.startsWith('Display ')) { str = format("Mostra {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>' + "Teclat 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', "Procés de control", format("Aturar el procés núm. {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("Port 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ó", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=3 selected>' + "Clau emmagatzemada" + '</option><option value=1>' + "Usuari contrasenya" + '</option><option value=2>' + "Nom d'usuari i clau" + '</option></select>');
            } else {
                x += addHtmlFormFloating("Autenticació", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=1 selected>' + "Usuari contrasenya" + '</option><option value=2>' + "Nom d'usuari i clau" + '</option></select>');
            }
            x += '<div id=d2userauth style=display:none>';
            x += addHtmlFormFloating("Nom d'usuari", '<input id=dp2user class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
            x += '</div>';
            x += '<div id=d2passauth style=display:none>';
            x += addHtmlFormFloating("Contrasenya", '<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">' + "Recordeu les credencials" + '</label>'); }
            x += '</div><div id=d2keyauth style=display:none>';
            x += addHtmlValue("Fitxer de claus", '<input type=file id=dp2key class="form-control" maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />' + '<div id=d2badkey style=font-size:x-small>' + "El fitxer de clau ha d'estar en format OpenSSH." + '</div>');
            x += addHtmlFormFloating("Contrasenya clau", '<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)>' + "Recorda l'usuari i la clau" + '</label>');
                x += addHtmlValue('', '<label><input id=dp2keep2 type=checkbox class="form-check-input me-2">' + "Recordar contrasenya" + '</label>');
            }
            x += '</div>';
            if (j.askkeypass) {
                x += '<div id=d2keyauth2 style=display:none>';
                x += addHtmlFormFloating("Contrasenya", '<input type=password id=dp2keypass2 class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
                x += '</div>';
            }
            setModalContent('xxAddAgent', "Autenticació", 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 connexió", 5000); break; }
                        case 'autherror': { p12setConsoleMsg("Error d'autenticació", 5000); break; }
                        case 'sessionerror': { p12setConsoleMsg("Sessió expirada", 5000); break; }
                        case 'sessiontimeout': { p12setConsoleMsg("Temps d'espera de la sessió", 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 estès", "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)", "Alternatiu (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', "Enganxa", '<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 s'han trobat fitxers" + '</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 d'autenticació", 5000); return; }
                case 'connectionerror': { p13setConsoleMsg("Error de connexió", 5000); return; }
                case 'sessionerror': { p13setConsoleMsg("Sessió expirada", 5000); return; }
                case 'sessiontimeout': { p13setConsoleMsg("Temps d'espera de la sessió", 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ó de fitxers", '<div style=margin:10px>' + "S'estan comprimint fitxers..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13fileOperationDialogEx(10));
                } else if ((data.msg == 'unzipping') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Operació de fitxers", '<div style=margin:10px>' + "S'està descomprimint el fitxer..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else if ((data.msg == 'unziperror') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Operació de fitxers", '<div style=margin:10px>' + "Error de descompressió" + '</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ó de fitxers", '<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)">' + "Arrel" + '</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', "Obriu Fitxer/Carpeta", "Esteu segur que voleu obrir aquest fitxer/carpeta a l'escriptori dels dispositius remots?");
            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', "Vés a la carpeta", '<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("Filtre", '<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=' + "Tanca" + ' style=float:right;width:80px;margin-left:5px onclick=p13findfileCancel()>';
            x += '<input id=filefind_dlgOkButton type=submit value=' + "Cerca" + ' style=float:right;width:80px onclick=p13findfileEx()><br /><br /></div>';
            setModalContent('xxAddAgent', "Cerca fitxers", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            p13findfileValidate();
            Q('d2findFilter').focus();
        }

        function p13findfileDown(e) { if (e.code == "Entra") { 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 tot";
                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 ? "Seleccioneu Cap" : "Seleccionar tot");
                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("Port 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', "Carpeta nova", '<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', "Fitxer nou", '<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>' + "Eliminació recursiva" + '</label><br>' : '<input type=checkbox class="form-check-input me-2" id=p13recdeleteinput style=\'display:none\'>';
            setModalContent('xxAddAgent', "Suprimeix", (cc > 1) ? (format("Vols suprimir {0} elements seleccionats?", cc) + rec) : ("Vols suprimir l'element seleccionat?" + 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', "Canvia el nom", '<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', "Penja el document", '<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 fitxers", "Només es poden editar fitxers de menys de 200 k."); }
                    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', "Descomprimiu a la carpeta", '<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', "Nom del fitxer 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("Confirmeu la còpia de {0} entrades a aquesta ubicació?", p13clipboard.length); } else { x = format("Vols confirmar la còpia d'1 entrada a aquesta ubicació?"); }
                } else {
                    if (p13clipboard.length > 1) { x = format("Vols confirmar el moviment de {0} entrades a aquesta ubicació?", p13clipboard.length); } else { x = format("Vols confirmar el moviment d'1 entrada a aquesta ubicació?"); }
                }
            }
            setModalContent('xxAddAgent', "Enganxa", 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("Mantenint {0} entrades per copiar" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Clar" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Conté 1 entrada per copiar" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Clar" + '</a>.');
                    }
                } else {
                    if (p13clipboard.length > 1) {
                        x = format("Mantingut {0} entrades per al moviment" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Clar" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Mantenint 1 entrada per al moviment" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Clar" + '</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', "Descarrega l'arxiu", '<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 = ["Embolcall: ON", "Embolcall: 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 = ["Mida: 100%", "Mida: 125%", "Mida: 150%", "Mida: 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ó: RAW", "Codificació: UTF8"][d4EditEncodingVal];
            putstore('editorEncoding', d4EditEncodingVal);
        }

        function d4ToggleLineBreak(update) {
            if (!update) {
                d4EditLineBreakVal = ++d4EditLineBreakVal % 3;
            }
            Q('d4LineBreakButton').value = ["Salt de línia: Windows (CR LF)", "Salt de línia: Linux (LF)", "Salt de línia: 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', "Penja el document", format((overWriteCount == 1) ? "La càrrega sobreescriurà 1 fitxer. Continuar?" : "La càrrega sobreescriurà {0} fitxers. 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', "Penja el document", '<div id=p13dfileName>' + "Connectant..." + '</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="' + "Aquesta és una sessió d'intercanvi de convidats" + '">' + 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 s'han trobat esdeveniments" + '</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', "Detalls de l'esdeveniment", 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: "Memòria", 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("Nom", EscapeHtml(node.rname), s); }
            if (hardware.windows && hardware.windows.osinfo && hardware.windows.osinfo.Description) { x += addDetailItem("Descripció", EscapeHtml(hardware.windows.osinfo.Description), s); }
            if (node.osdesc) { x += addDetailItem("Versió", EscapeHtml(node.osdesc), s); }
            if (hardware.windows && hardware.windows.osinfo) {
                var m = hardware.windows.osinfo;
				if(m.BuildRevision){ x += addDetailItem("Número de compilació", 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("Última hora d'arrencada", date);
                }
                if(m.Domain){ x += addDetailItem((m.PartOfDomain ? "Domini" : "Grup de treball"), EscapeHtml(m.Domain), s); }
                if(m.DomainState > 0){ x += addDetailItem("Estat del domini", (domainStates[m.DomainState] ? domainStates[m.DomainState] : "Desconegut"), s); }
            }
            if (hardware.linux && hardware.linux.LastBootUpTime) {
                var lastBootUpTime = new Date(hardware.linux.LastBootUpTime);
                const date = printDateTime(lastBootUpTime);
                x += addDetailItem("Última hora d'arrencada", 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ó del nucli", EscapeHtml(hardware.linux.kernel_release), s); }
            if (hardware.linux && hardware.linux.kernel_build) { x += addDetailItem("Compilació del nucli", 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("Última hora d'arrencada", date);
            }
            if (x != '') { sections.push({ name: "Sistema operatiu", 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("Agent de malla", str);
                }
                if (node.firstconnect) { x += addDetailItem("Primera connexió d'agent", printDateTime(new Date(node.firstconnect))); }
                if ((node.conn & 1) != 0) {
                    x += addDetailItem("Última connexió d'agent", "Connectat ara");
                } else {
                    if (node.lastconnect) { x += addDetailItem("Última connexió d'agent", printDateTime(new Date(node.lastconnect))); }
                }
                if (node.lastaddr) {
                    var splitip = node.lastaddr.split(':');
                    if (splitip.length > 2) {
                        // IPv6
                        x += addDetailItem("Adreça de l'últim agent", '<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("Adreça de l'últim agent", splitip[0]);
                        } else {
                            x += addDetailItem("Adreça de l'últim agent", '<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("Temps de compilació", isNaN(d) ? EscapeHtml(hardware.agentvers.compileTime) : printDateTime(new Date(d)));
                    }
                }
                if (hardware.time != null) {
                    x += addDetailItem("Última actualització de detalls", printDateTime(new Date(hardware.time)));
                }
                if (x != '') { sections.push({ name: "Agent de malla", html: x, img: 'meshagent64.png' }); }
            }

            // Mobile
            if (hardware.mobile) {
                var x = '';
                if (hardware.mobile.brand && hardware.mobile.model) { x += addDetailItem("Model", EscapeHtml(hardware.mobile.brand + ', ' + hardware.mobile.model), s); }
                if (hardware.mobile.device) { x += addDetailItem("Dispositiu", EscapeHtml(hardware.mobile.device), s); }
                if (hardware.mobile.bootloader) { x += addDetailItem("Carregador d'arrencada", EscapeHtml(hardware.mobile.bootloader), s); }
                if (hardware.mobile.id) { x += addDetailItem("Identificador", EscapeHtml(hardware.mobile.id), s); }
                if (hardware.mobile.host) { x += addDetailItem("Nom d'amfitrió", EscapeHtml(hardware.mobile.host), s); }
                if (hardware.mobile.androidapi && hardware.mobile.androidrelease) { x += addDetailItem("Versió d'Android", EscapeHtml(hardware.mobile.androidrelease + ', API Level ' + hardware.mobile.androidapi), s); }
                if (x != '') { sections.push({ name: "Dispositiu mòbil", 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ó", 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}, passarel·la: {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}, passarel·la: {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: "Treball en xarxa", 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ó", 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}, passarel·la: {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("Velocitat de la interfície", 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("Passarel·la: {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>' + "Servidors DNS" + '</b>', EscapeHtml(hardware.network.dns.join(",")));
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Treball en xarxa", html: x, img: 'networking64.png' }); }
            }

            // Attribute: Intel AMT
            if (node.intelamt != null) {
                var x = '';
                x += addDetailItem("Versió", (node.intelamt.ver) ? ('v' + EscapeHtml(node.intelamt.ver)) : ('<i>' + "Desconegut" + '</i>'), s);
                var provisioningStates = { 0: nobreak("No activat"), 1: nobreak("Preactivació"), 2: nobreak("Activat") };
                var provisioningMode = '';
                if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Mode de control de client (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Mode de control d'administrador (ACM)"); } }
                x += addDetailItem("Estat de subministrament", ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('<i>' + "Desconegut" + '</i>')) + provisioningMode, s);
                x += addDetailItem("Seguretat", (node.intelamt.tls == 1) ? "Protegit mitjançant TLS" : "TLS no està configurat", s);
                // Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
                x += addDetailItem("Credencials d'administrador", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0))) ? "Desconegut" : "Conegut", s);
                if (x != '') {
                    if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
                        sections.push({ name: "Gestió estàndard d'Intel&reg; (Intel&reg; SM)", html: x, img: 'amt64.png' });
                    } else {
                        sections.push({ name: "Tecnologia de gestió activa Intel&reg; (Intel&reg; AMT)", html: x, img: 'amt64.png' });
                    }
                }
            }

            if (hardware.identifiers) {
                var x = '', ident = hardware.identifiers;
                // BIOS
                if (ident.bios_vendor) { x += addDetailItem("Venedor", EscapeHtml(ident.bios_vendor), s); }
                if (ident.bios_version) { x += addDetailItem("Versió", EscapeHtml(ident.bios_version), s); }
                if (ident.bios_serial) { x += addDetailItem("Serial", 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("Venedor", EscapeHtml(ident.board_vendor), s); }
                if (ident.board_name) { x += addDetailItem("Nom", EscapeHtml(ident.board_name), s); }
                if (ident.board_serial && (ident.board_serial != '')) { x += addDetailItem("Serial", EscapeHtml(ident.board_serial), s); }
                if (ident.board_version) { x += addDetailItem("Versió", 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 base", html: x, img: 'motherboard64.png' }); }

                // System
                x = '';
                if (ident.chassis_manufacturer) { x += addDetailItem("Manufacturer", EscapeHtml(ident.chassis_manufacturer), s); }
                if (ident.product_name) { x += addDetailItem("Nom del producte", EscapeHtml(ident.product_name), s); }
                if (ident.chassis_serial) { x += addDetailItem("Serial", EscapeHtml(ident.chassis_serial), s); }
                if (ident.chassis_assettag) { x += addDetailItem("Etiqueta d'actiu", 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ó", EscapeHtml(tpm.ManufacturerVersion), s); }
                if (tpm.IsActivated != null) { x += addDetailItem("Activat", (tpm.IsActivated ? "Sí" : "No"), s); }
                if (tpm.IsEnabled != null) { x += addDetailItem("Habilitat", (tpm.IsEnabled ? "Sí" : "No"), s); }
                if (tpm.IsOwned != null) { x += addDetailItem("Owned", (tpm.IsOwned ? "Sí" : "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 : "Desconegut") + '</b></div>';
                    if (battery.CycleCount) { x += addDetailItem("Cycle Count", EscapeHtml(battery.CycleCount), s); }
                    if (battery.FullChargedCapacity) { x += addDetailItem("Full Charged Capacity", format("{0} mWh", battery.FullChargedCapacity), s); }
                    if (battery.EstimatedRuntime) { x += addDetailItem("Estimated Runtime", format("{0} minuts", Math.floor((battery.EstimatedRuntime / 60))), s); }
                    if (battery.Chemistry) { x += addDetailItem("Chemistry", EscapeHtml(battery.Chemistry), s); }
                    if (battery.DesignedCapacity) { x += addDetailItem("Design Capacity", format("{0} mWh", battery.DesignedCapacity), s); }
                    if (battery.ManufactureDate) { x += addDetailItem("Manufacture Date", EscapeHtml(battery.ManufactureDate), s); }
                    if (battery.ManufactureName) { x += addDetailItem("Manufacture Name", EscapeHtml(battery.ManufactureName), s); }
                    if (battery.SerialNumber) { x += addDetailItem("Serial Number", EscapeHtml(battery.SerialNumber), s); }
                    if (battery.ChargeRate) { x += addDetailItem("Charge Rate", format("{0} mW", battery.ChargeRate), s); }
                    if (battery.Charging != null) { x += addDetailItem("Charging", (battery.Charging ? "Sí" : "No"), s); }
                    if (battery.DischargeRate) { x += addDetailItem("Discharge Rate", format("{0} mW", battery.DischargeRate), s); }
                    if (battery.Discharging != null) { x += addDetailItem("Discharging", (battery.Discharging ? "Sí" : "No"), s); }
                    if (battery.RemainingCapacity) { x += addDetailItem("Remaining Capacity", format("{0} mWh", battery.RemainingCapacity), s); }
                    if (battery.Voltage) { x += addDetailItem("Voltage", format("{0} V", (battery.Voltage / 1000)), s); }
                    if (battery.Health) { x += addDetailItem("Salut", format("{0} %", battery.Health), s); }
                    if (battery.BatteryCharge) { x += addDetailItem("Carrega de la bateria", format("{0} %", battery.BatteryCharge), s); }
                    x += '</div>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Battery", 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("Capacitat / Velocitat", EscapeHtml(format("{0} Mb, {1} Mhz", (m.Capacity / 1024 / 1024), m.Speed)), s); }
                        else if (m.Capacity) { x += addDetailItem("Capacitat", EscapeHtml(format("{0} Mb", (m.Capacity / 1024 / 1024))), s); }
                        if (m.PartNumber) { x += addDetailItem("Número de part", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Memòria", 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("Capacitat / Velocitat", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Capacitat", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Número de part", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Memòria", 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("Capacitat / Velocitat", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Capacitat", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Número de part", EscapeHtml((m.Manufacturer && m.Manufacturer != '') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';
                    if (x != '') { sections.push({ name: "Memòria", 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("Model", 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("Capacitat", format("{0} Mb", Math.floor(m.Size / 1024 / 1024)), s); }
                            if (typeof m.Size == 'string') { x += addDetailItem("Capacitat", 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("Estat", EscapeHtml(foundObject.Status), s);
                        }
                        x += '</div>';
                    }
                }
                x += '</table>';

                if (x != '') { sections.push({ name: "Emmagatzematge", 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: "Desconegut", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" };
                const conversionStatus = { 1: "Totalment encriptat", 2: "Xifratge en curs", 3: "Decryption In Progress", 4: "Encryption Paused", 5: "Decryption Paused" };
                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("Capacitat", 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("Capacitat restant", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Extraïble" : (m.cdrom == true ? "CD ROM" : ''));
                        if (m.dType != null) { type = driveType.hasOwnProperty(m.dType) ? driveType[m.dType] : "Desconegut"; }
                        x += addDetailItem("Sistema de fitxers", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Desconegut" : 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 = "Tancat";
                            } else {
                                bitlockerState = (conversionStatus.hasOwnProperty(m.volumeStatus) ? conversionStatus[m.volumeStatus] : "Desconegut");
                            }
                        }
                        bitlockerState += (encMethod.hasOwnProperty(m.encryptionMethod) ? ' / ' + encMethod[m.encryptionMethod] : ' / ' + "Desconegut");
                        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: "Volums d'emmagatzematge", 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("Capacitat", 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("Capacitat restant", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Extraïble" : (m.cdrom == true ? "CD ROM" : ''));
                        x += addDetailItem("Sistema de fitxers", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Desconegut" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Volums d'emmagatzematge", 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("Capacitat", EscapeHtml(m.size), s);
                    }
                    if (m.available) {
                        x += addDetailItem("Capacitat restant", EscapeHtml(m.available), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Extraïble" : (m.cdrom == true ? "CD ROM" : ''));
                        x += addDetailItem("Sistema de fitxers", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Desconegut" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Volums d'emmagatzematge", 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 hi ha informació per a aquest dispositiu.");
            } 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) : "Desconegut") + '</p>';
            x += '<p>' + "Contrasenya de recuperació" + '</p><p style=user-select:text;font-weight:bold>' + (password ? decodeURIComponent(password) : "Desconegut") + '</p></div>';
            setModalContent('xxAddAgent', decodeURIComponent(drive) + ': ' + "Informació de BitLocker", 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>' + "Carregant aplicacions d'escriptori..." + '</span>');
            QH('p18html', '<div style="text-align:center;padding:50px;color:gray">' + "Carregant programari instal·lat..." + '</div>');
            if ((currentNode.conn & 1) == 0) {
                QH('p18html', '<div style="text-align:center;padding:50px;color:#cc0000"><b>' + "El dispositiu està fora de línia." + '</b></div>');
                QH('p18Status', '<span>' + "Fora de línia" + '</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>' + "Carregant aplicacions de la botiga..." + '</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> ' + "Aplicacions d'escriptori" + ' (' + 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">' + "Nom" + '</th>';
            x += '<th style="text-align:left;width:120px">' + "Versió" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Editor" + '</th>';
            x += '<th style="text-align:left;width:100px">' + "Data d'instal·lació" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Ubicació" + '</th>';
            x += '<th style="text-align:center;width:100px">' + "Accions" + '</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 hi ha aplicacions que coincideixin" + '"' + EscapeHtml(searchTerm) + '"' : "No s'han trobat aplicacions d'escriptori";
                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> ' + "Aplicacions 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">' + "Nom" + '</th>';
				x += '<th style="text-align:left;width:120px">' + "Versió" + '</th>';
				x += '<th style="text-align:left;width:250px">' + "Paquet" + '</th>';
				x += '<th style="text-align:center;width:100px">' + "Àmbit" + '</th>';
				x += '<th style="text-align:center;width:80px">' + "Accions" + '</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 hi ha aplicacions de la botiga que coincideixin" + '"' + EscapeHtml(searchTerm) + '"' : "No s'han trobat aplicacions de la botiga";
					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="' + "Instal·lat només per a l'usuari actual" + '">' + "Usuari" + '</span>';
							} else if (app.scope.indexOf('System') >= 0) {
								scopeBadge = '<span class="badge bg-info" title="' + "Instal·lat per a tots els usuaris" + '">' + "Sistema" + '</span>';
							}
							if (app.scope.indexOf('Prov') >= 0) {
								scopeBadge += ' <span class="badge bg-secondary" title="' + "Aprovisionat per a nous usuaris" + '">' + "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 = "Escriptori:" + ' ' + shownDesktop + '/' + totalDesktop;
            if (showStoreApps) {
                statusText += ' | ' + "Botiga:" + ' ' + 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 hi ha cap ordre de desinstal·lació disponible" + '</span>');
                return;
            }
            var dialogContent = '<div style="max-width:500px">';
            dialogContent += '<p><b>' + "Voleu desinstal·lar aquest programari?" + '</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>' + "Es realitzarà una desinstal·lació silenciosa" + '</i></p>';
            dialogContent += '</div>';
            setModalContent('xxAddAgent', "Desinstal·lar programari", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
                QH('p18Status', '<span>' + "Desinstal·lant..." + '</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', "Desinstal·lació iniciada. Feu clic a Actualitza per actualitzar"); }, 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>' + "Voleu eliminar aquesta aplicació de la botiga?" + '</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ó de la botiga", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
				QH('p18Status', '<span>' + "Eliminant..." + '</span>');
				meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'uninstallstoreapp', value: '"' + cmd.replace(/"/g, '\\"') + '"' });
				setTimeout(function() { QH('p18Status', "Ordre d'eliminació enviada. Feu clic a Actualitza per actualitzar"); }, 2000);
			});
		}

        function handleUninstallResponse(data) {
            try {
                var result = JSON.parse(data);
                if (result.success) {
                    QH('p18Status', '<span>✓ ' + "Èxit" + '</span> ' + "Feu clic a Actualitza per actualitzar");
                } 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', ' - ' + "La meva consola del 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) ? "L'agent està en línia" : "L'agent està fora de línia"
                    if ((consoleNode.conn & 16) != 0) { onlineText += ", MQTT està en línia" }
                    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', "Accés denegat");
                    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>' + "Carregueu el nucli del servidor predeterminat" + '</option>' +
                    '<option value=2>' + "Netegeu el nucli" + '</option>' +
                    '<option value=3>' + "Carregueu un fitxer bàsic" + '</option>' +
                    '<option value=4>' + "Agent de desconnexió suau" + '</option>' +
                    '<option value=5>' + "Agent de desconnexió dura" + '</option>' +
                    '<option value=6>' + "Carregueu el nucli de recuperació" + '</option>' +
                    '<option value=7>' + "Carregueu un nucli petit" + '</option>' +
                    '<option value=8>' + "Restart agent service" + '</option>' +
                    '<option value=9>' + "Força l'actualització de l'agent" + '</option></select>';
                setModalContent('xxAddAgent', "Realitza l'acció de l'agent", addHtmlFormFloating("Acció", 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', "Carregueu Mesh Agent Core", '');
            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', "Inicis de sessió anteriors", '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', "Gestiona la imatge del compte", 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èfon verificat" + '</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() />' + "Elimina el número de telèfon" + '</label></div>';
                setModalContent('xxAddAgent', "Notificacions telefòniques", 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>' + "Introduïu el vostre número de telèfon compatible amb SMS. Un cop verificat, el número es pot utilitzar per a la verificació d'inici de sessió i altres notificacions.";
                x += '<br /><br /><div style=width:100%;text-align:center>' + "Número de telèfon:" + ' <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', "Notificacions telefòniques", 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>' + "Mànec verificat" + '</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() />' + "Elimina els missatges" + '</label></div>';
                setModalContent('xxAddAgent', "Notificacions de missatgeria", 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>' + "Introdueix el teu servei de missatgeria i gestiona. Un cop verificat, aquest servidor us pot enviar la verificació d'inici de sessió i altres notificacions." + '<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>' + "Telegrama" + '</option>'; }
                if ((serverinfo.userMsgProviders & 4) != 0) { y += '<option value=4>' + "Discòrdia" + '</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>' + "Empènyer" + '</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>' + "Fluix" + '</option>'; }
                y += '</select>';
                x += '<table><tr><td>' + "Servei" + '<td>' + y;
                x += '<tr><td>' + "Gestionar" + '<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">' + "Uneix-te a aquest servidor de Discord per rebre notificacions." + '</a></div>'; }
                x += '<div id=d2callmebotinfo style=display:none><br /><a href=https://www.callmebot.com/blog/free-api-signal-send-messages/ target="_callmebot">' + "Senyal" + '</a>, <a href=https://www.callmebot.com/blog/free-api-whatsapp-messages/ target="_callmebot">' + "què tal" + '</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">' + "Telegrama" + '</a></div>';
                x += '<div id=d2pushoverinfo style=display:none><br /><a href=https://pushover.net/ target="_pushover">' + "Informació a Pushover.net" + '</a></div>';
                x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Servei gratuït a ntfy.sh" + '</a></div>';
                x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Configuració de Slack Webhook" + '</a></div>';
                xxdialogTag = 'VerifyMessaging';
                setModalContent('xxAddAgent', "Notificacions de missatgeria", 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'] = "Nom d'usuari: 0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "nom d'usuari@server.com"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Clau d'usuari"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Tema"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "nom d'usuari@sample.com"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Nom d'usuari"; }
            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ó de correu electrònic", "Quan estigui activat, a cada inici de sessió, se us donarà l'opció de rebre un testimoni d'inici de sessió al vostre compte de correu electrònic per a més seguretat." + '<br /><br /><label><input id=email2facheck type=checkbox class="form-check-input me-2" ' + (emailU2Fenabled ? 'checked' : '') + '/>' + "Activa l'autenticació de dos factors de correu electrònic." + '</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ó d'autenticació", ('<div id=d2optinfo>' + "Carregant..." + '</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ó d'autenticació", "Confirmeu l'eliminació de l'inici de sessió en dos passos de l'aplicació d'autenticació?");
            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', "Dispositiu d'autenticació", "Confirmeu l'eliminació del dispositiu d'autenticació 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', "Dispositiu d'autenticació", '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 = "Seleccioneu un dispositiu per registrar-vos per a l'autenticació de notificacions push. Un cop seleccionat, el dispositiu demanarà confirmació." + '<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("Dispositiu", y);
                    setModalContent('xxAddAgent', "Dispositiu d'autenticació", 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 = "Escriviu el nom de la clau que voleu afegir." + '<br /><br />';
                x += addHtmlFormFloating("Nom de la clau", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "La meva clau" + '" onkeyup=account_addhkeyValidate(event,2) />');
            } else if (type == 2) {
                var x = "Escriviu un nom de clau, seleccioneu la casella OTP i premeu el botó de la YubiKey&trade;." + '<br /><br />';
                x += addHtmlFormFloating("Nom de la clau", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "La meva clau" + '" 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', "Afegeix clau de seguretat", 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', "Afegeix clau de seguretat", '<br />' + "S'està comprovant..." + '<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': "Afrikàans", 'sq': "albanès", 'ar': "Àrab (estàndard)", 'ar-dz': "Àrab (Algèria)", 'ar-bh': "Àrab (Bahrain)", 'ar-eg': "Àrab (Egipte)", 'ar-iq': "Àrab (Iraq)", 'ar-jo': "Àrab (Jordània)", 'ar-kw': "Àrab (Kuwait)", 'ar-lb': "Àrab (Líban)", 'ar-ly': "Àrab (Líbia)", 'ar-ma': "Àrab (Marroc)", 'ar-om': "Àrab (Oman)", 'ar-qa': "Àrab (Qatar)", 'ar-sa': "Àrab (Aràbia Saudita)", 'ar-sy': "Àrab (Síria)", 'ar-tn': "Àrab (Tunísia)", 'ar-ae': "Àrab (EAU)", 'ar-ye': "Àrab (Iemen)", 'an': "aragonès", 'hy': "armeni", 'as': "assameses", 'ast': "asturià", 'az': "azerbaidjana", 'eu': "basc", 'bg': "búlgar", 'be': "Bielorús", 'bn': "bengalí", 'bs': "bosnià", 'br': "bretó", 'my': "birmà", 'ca': "català", 'ch': "Chamorro", 'ce': "Txetxè", 'zh': "xinès", 'zh-hk': "Xinès (Hong Kong)", 'zh-cn': "Xinès (RPC)", 'zh-sg': "Xinès (Singapur)", 'zh-tw': "Xinès (Taiwan)", 'cv': "Txuvaix", 'co': "Cors", 'cr': "Cree", 'hr': "croat", 'cs': "txec", 'da': "danès", 'nl': "holandès (estàndard)", 'nl-be': "holandès (belga)", 'en': "Anglès", 'en-au': "Anglès (Austràlia)", 'en-bz': "Anglès (Belice)", 'en-ca': "Anglès (Canadà)", 'en-ie': "Anglès (Irlanda)", 'en-jm': "Anglès (Jamaica)", 'en-nz': "Anglès (Nova Zelanda)", 'en-ph': "Anglès (Filipines)", 'en-za': "Anglès (Sud-àfrica)", 'en-tt': "Anglès (Trinitat i Tobago)", 'en-gb': "Anglès (Regne Unit)", 'en-us': "Anglès (Estats Units)", 'en-zw': "Anglès (Zimbabwe)", 'eo': "esperanto", 'et': "estonià", 'fo': "feroès", 'fa': "Farsi (persa)", 'fj': "Fijians", 'fi': "finès", 'fr': "francès (estàndard)", 'fr-be': "francès (Bèlgica)", 'fr-ca': "francès (Canadà)", 'fr-fr': "francès (França)", 'fr-lu': "francès (Luxemburg)", 'fr-mc': "francès (Mònaco)", 'fr-ch': "francès (Suïssa)", 'fy': "frisó", 'fur': "friül", 'gd': "Gaèlic (escocès)", 'gd-ie': "Gaèlic (irlandès)", 'gl': "Galacià", 'ka': "georgiana", 'de': "alemany (estàndard)", 'de-at': "alemany (Àustria)", 'de-de': "alemany (Alemanya)", 'de-li': "alemany (Liechtenstein)", 'de-lu': "alemany (Luxemburg)", 'de-ch': "alemany (Suïssa)", 'el': "grec", 'gu': "Gujurati", 'ht': "haitià", 'he': "hebreu", 'hi': "hindi", 'hu': "hongarès", 'is': "islandès", 'id': "indonesi", 'iu': "Inuktitut", 'ga': "irlandès", 'it': "Italià (estàndard)", 'it-ch': "Italià (Suïssa)", 'ja': "japonès", 'kn': "Kannada", 'ks': "Caixmir", 'kk': "Kazakh", 'km': "Khmer", 'ky': "kirguiz", 'tlh': "Klingon", 'ko': "coreà", 'ko-kp': "coreà (Corea del Nord)", 'ko-kr': "coreà (Corea del Sud)", 'la': "llatí", 'lv': "letó", 'lt': "lituà", 'lb': "luxemburguesa", 'mk': "FYRO Macedoni", 'ms': "Malai", 'ml': "malayalam", 'mt': "maltès", 'mi': "Maori", 'mr': "Marathi", 'mo': "Moldàvia", 'nv': "Navajo", 'ng': "Ndonga", 'ne': "nepalí", 'no': "noruec", 'nb': "noruec (Bokmal)", 'nn': "noruec (Nynorsk)", 'oc': "occità", 'or': "Oriya", 'om': "Oromo", 'fa-ir': "Persa/Iran", 'pl': "polonès", 'pt': "portuguès", 'pt-br': "portuguès (Brasil)", 'pa': "panjabi", 'pa-in': "Panjabi (Índia)", 'pa-pk': "Panjabi (Pakistan)", 'qu': "Quechua", 'rm': "retoromànic", 'ro': "romanès", 'ro-mo': "romanès (Moldavia)", 'ru': "rus", 'ru-mo': "Rus (Moldvia)", 'sz': "Sami (lapons)", 'sg': "Sango", 'sa': "sànscrit", 'sc': "sard", 'sd': "Sindhi", 'si': "Singalès", 'sr': "serbi", 'sk': "eslovac", 'sl': "eslovè", 'so': "Somani", 'sb': "sòrab", 'es': "espanyol", 'es-ar': "espanyol (Argentina)", 'es-bo': "espanyol (Bolívia)", 'es-cl': "espanyol (Xile)", 'es-co': "espanyol (Colòmbia)", 'es-cr': "Espanyol (Costa Rica)", 'es-do': "Espanyol (República Dominicana)", 'es-ec': "espanyol (Equador)", 'es-sv': "espanyol (El Salvador)", 'es-gt': "espanyol (Guatemala)", 'es-hn': "espanyol (Hondures)", 'es-mx': "espanyol (Mèxic)", 'es-ni': "espanyol (Nicaragua)", 'es-pa': "espanyol (Panamà)", 'es-py': "espanyol (Paraguai)", 'es-pe': "espanyol (Perú)", 'es-pr': "Espanyol (Puerto Rico)", 'es-es': "espanyol (Espanya)", 'es-uy': "Espanyol (Uruguai)", 'es-ve': "espanyol (Veneçuela)", 'sx': "Sutu", 'sw': "suahili", 'sv': "suec", 'sv-fi': "Suec (Finlàndia)", 'sv-sv': "Suec (Suècia)", 'ta': "Tamil", 'tt': "Tàrtar", 'te': "Teluga", 'th': "tailandès", 'tig': "Tigre", 'ts': "Tsonga", 'tn': "Tswana", 'tr': "turc", 'tk': "turcomà", 'uk': "Ucraïnès", 'hsb': "alt sòrab", 'ur': "Urdú", 've': "Venda", 'vi': "vietnamita", 'vo': "Volapuk", 'wa': "Való", 'cy': "gal·lès", 'xh': "Xhosa", 'ji': "Yiddish", 'zu': "Zulu" };
        var loclistex = { 'zh-chs': "Xinès (simplificat)", 'zh-cht': "Xinès (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 d'usuari" + '</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 += "Canviar l'idioma requerirà una actualització de la pàgina." + '<br /><br />';
                var z = '<select id=d2langselect class="form-select"><option value="*">' + "Valor del navegador d'usuari" + '</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("Llenguatge", z);
            }
            y += addHtmlFormFloating("Dates i hora", x);

            if ((userinfo.siteadmin == 0xFFFFFFFF) && (domain == '')) {
                y += '<br /><a rel="noreferrer noopener" target="_blank" href="translator.htm">' + "Ajuda a traduir MeshCentral" + '</a>';
            }

            setModalContent('xxAddAgent', "Configuració de localització", 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 = "Creeu un nom d'usuari i una contrasenya temporals que es poden utilitzar com a inici de sessió alternatiu al vostre compte. Això és útil per permetre que eines o altres serveis accedeixin al vostre compte." + '<br /><br />';
            var options = { 0: "Il·limitat", 1: "1 minut", 5: "5 minuts", 10: "10 minuts", 15: "15 minuts", 30: "30 minuts", 45: "45 minuts", 60: "60 minuts", 120: "2 hores", 240: "4 hores", 480: "8 hores", 720: "12 hores", 960: "16 hores", 1440: "24 hores", 2880: "2 dies", 5760: "4 dies", 10080: "7 dies" }
            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="' + "Nom del testimoni" + '" onchange=account_createLoginTokenValidate() onkeyup=account_createLoginTokenValidate()>';
            x += '<label for=d2tokenName>' + "Nom del testimoni" + '</label>';
            x += '</div>';
            x += addHtmlFormFloating("Temps d'expiració", '<select class="boxsize form-select" id=d2tokenExpire>' + y + '</select>')

            setModalContent('xxAddAgent', "Crea un testimoni d'inici de sessió", 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 />' + "So de notificació" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyGroupName class="form-check-input me-2" type=checkbox />' + "Mostra el nom del grup de dispositius" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Connexions de dispositius" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconnexions del dispositiu" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Esdeveniments d'escriptori i sèrie Intel&reg; AMT" + '</label></div>';

            setModalContent('xxAddAgent', "Configuració de notificacions", 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 = "Feu clic a D'acord per enviar un correu de verificació a:" + '<br /><div style=padding:8px><b>' + EscapeHtml(userinfo.email) + '</b></div>' + "Espereu uns minuts per rebre la verificació.";
            setModalContent('xxAddAgent', "Verificació del correu electrònic", 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 = "Canvia l'adreça de correu electrònic del teu compte 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', "Canvi d'adreça de correu electrònic", 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 = "Per suprimir aquest compte, escriviu la contrasenya del compte als dos quadres següents i premeu D'acord." + '<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', "Esborrar compte", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                dialogclose(1);
            });

            account_validateDeleteAccount();
            Q('apassword1').focus();
            return false;
        }

        function account_showChangePassword() {
            if (xxdialogMode) return false;
            var x = "Canvieu la contrasenya del vostre compte introduint la contrasenya antiga i la nova dues vegades als quadres següents.";
            if (features & 0x00010000) { " Es pot utilitzar una pista de contrasenya, però no es recomana."; }
            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>' + "Requisits:" + 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', "Canvia la contrasenya", 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: "Per defecte" },
                { 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">' + "Select a theme" + ':</label>';
            x += '<select id="theme-switcher" class="form-select" onchange="account_switchThemeEx()">';
            // Add "Recent Themes" optgroup
            if (lastThemes.length > 0) {
                x += '<optgroup label="' + "Recent Themes" + '"">';
                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="' + "All Themes" + '">';
            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', "Canvia el 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', "Nou grup de dispositius", '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', "Seguretat del compte", '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', "Seguretat del compte", '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 = "Creeu un grup de dispositius nou amb les opcions següents." + '<br /><br />';
            var localGroupType = '';

            x += addHtmlFormFloating("Nom", '<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>' + "Dispositius locals, sense agent" + '</option>'; }
            if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=103>' + "No s'ha retransmès cap dispositiu d'agent a través de l'agent" + '</option>'; }
            if (features2 & 0x10000) {
                if ((features & 1) == 0) { localGroupType += '<option value=4>' + "IP-KVM / Dispositiu d'alimentació" + '</option>'; }
                if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=104>' + "IP-KVM / Dispositiu d'alimentació retransmès a través de l'agent" + '</option>'; }
            }
            x += addHtmlFormFloating("Tipus", '<select id=dp2meshtype class=form-select onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=2>' + "Gestionar mitjançant un agent de programari" + '</option><option value=1>' + "Només Intel&reg; AMT, sense agent" + '</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("Dispositiu de relé", '<select id=d2devrelay onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select >' + relayDevices2.join('') + '</select>');
                x += '</div>';
            }
            x += addHtmlFormFloating("Descripció", '<textarea id=dp2meshdesc maxlength=1024 class=form-control></textarea>');
            x += '<div id=d2ipkvm style=display:none><hr />';
            x += addHtmlFormFloating("Model", '<select id=dp2ipkvmmodel onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select ><option value=1>' + "Domini Raritan KX III" + '</option><option value=2>' + "Interruptor d'alimentació web 7" + '</option></select>');
            x += addHtmlFormFloating("Nom d'amfitrió", '<input id=dp2ipkvmhost maxlength=128 placeholder="Hostname" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Nom d'usuari", '<input id=dp2ipkvmuser maxlength=128 placeholder="Username" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Contrasenya", '<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', "Nou grup de dispositius", 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 == "Entra") && (Q('dp2meshname').value.length > 0)) { Q('dp2meshtype').focus(); }
            if ((x == 2) && (e != null) && (e.key == "Entra")) { 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>' + "Fort" + '<span>'; } else if (passStrength >= 60) { r = '<span style=color:blue>' + "Bé" + '<span>'; } else { r = '<span style=color:red>' + "Feble" + '<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 = "Drets Parcials";
                if (meshrights == 0xFFFFFFFF) rights = "Administrador total"; else if (meshrights == 0) rights = "Sense Drets";

                // 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>' + "Fitxes d'inici de sessió actius" + '</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> ' + "Nou" + '</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>' + "Nom" + '</th><th scope=col style=text-align:left>' + "Nom d'usuari" + '</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="' + "Elimina el testimoni d'inici de sessió" + '" 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 dia {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 = "Vols confirmar l'eliminació d'aquest testimoni d'inici de sessió?" + '<br /><br />';
            x += addHtmlValue("Nom", EscapeHtml(token.name));
            x += addHtmlValue("Nom d'usuari", EscapeHtml(token.tokenUser));
            setModalContent('xxAddAgent', "Elimina el testimoni d'inici de sessió", 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 aquest servidor per carregar còpies de seguretat automàticament a Google Drive. Comenceu creant i introduint un ClientID i ClientSecret de Google Drive per al vostre compte." + '</div><br />';
                x += addHtmlValue("Credencials", '<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 de client", '<input id=gdclientid class="form-control" placeholder="xxxxxxxx.apps.googleusercontent.com" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                x += addHtmlFormFloating("Secret del client", '<input id=gdclientsecret class="form-control" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                setModalContent('xxAddAgent', "Còpia de seguretat de 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>' + "La còpia de seguretat de Google Drive està activa actualment." + '</div>';
                x += '<br /><label><input id=gdcheck type=checkbox class="form-check-input me-2" onchange=server_setupGoogleDriveBackupCheck2() />' + "Elimina la configuració" + '</label>';
                setModalContent('xxAddAgent', "Còpia de seguretat de 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 = "Introduïu una contrasenya opcional per a la còpia de seguretat" + '<br /><br />';
            x += '<tr><td align=right>' + nobreak("Contrasenya: ") + '</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') + ' /> ' + "Utilitza la contrasenya del sistema" + '</label></div>';
            setModalContent('xxAddAgent', "Descarrega la còpia de seguretat 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 mitjançant una còpia de seguretat," + ' <span style=color:red>' + "això suprimirà les dades del servidor existent" + '</span> ' + "Fes això només si saps el que estàs fent." + '<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("Contrasenya de còpia de seguretat: ") + '</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') + ' /> ' + "Utilitza la contrasenya del sistema" + '</label></div>';
            setModalContent('xxAddAgent', "Restaura el 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', "Restaura el servidor", "S'està carregant la còpia de seguretat");
            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', "Restaura el servidor", "Error de xarxa: " + response.status);
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        QE('idx_dlgOkButton', true);
                    }
                })
                .catch((error) => {
                    setModalContent('xxAddAgent', "Restaura el 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ó MeshCentral", "Carregant...");
            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 != "Desconegut") && (xxdialogTag.current != xxdialogTag.stable) && (latestCheck == false)));
            QE('d2updateCheck2', ((xxdialogTag.latest != "Desconegut") && (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', "Errors del servidor", "Carregant...");
            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>' + "Cap" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span role=button tabindex=0 title="' + "Feu clic aquí per editar el nom del grup de dispositius" + '" 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("Desconegut #{0}", currentMesh.mtype);
            if (currentMesh.mtype == 1) meshtype = "Només Intel&reg; AMT, sense agent";
            if (currentMesh.mtype == 2) meshtype = "Gestionat mitjançant un agent de programari";
            if (currentMesh.mtype == 3) { if (currentMesh.relayid == null) { meshtype = "Dispositius locals, sense agent"; } else { meshtype = "No s'ha retransmès cap dispositiu d'agent a través de l'agent"; } }
            if (currentMesh.mtype == 4) { if (currentMesh.relayid == null) { meshtype = "Dispositiu IP-KVM"; } else { meshtype = "Dispositiu IP-KVM retransmès a través de l'agent"; } if (currentMesh.kvm.model == 1) { meshtype += ', ' + 'Raritan KX III'; } }

            var x = '';
            if ((args.hide & 8) != 0) { x += addHtmlValue("Nom", mname); } // If title bar is hidden, display the mesh name here
            x += addHtmlValue("Descripció", addLinkConditional(((currentMesh.desc && currentMesh.desc != '') ? EscapeHtml(currentMesh.desc) : ('<i>' + "Cap" + '</i>')), 'p20editmesh(2)', (meshrights & 1) != 0));

            // Display group type
            x += addHtmlValue("Tipus", 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>' + "Desconegut" + '</i>';
                var relayNode = getNodeFromId(currentMesh.relayid);
                if (relayNode != null) { relayName = EscapeHtml(relayNode.name) + ' (' + EscapeHtml(relayNode.meshnamel) + ')'; }
                x += addHtmlValue("Dispositiu de relé", addLinkConditional(relayName, 'p20editmeshrelay()', (meshrights & 1) != 0));
            }

            // Display IP-KVM information if needed
            if (currentMesh.mtype == 4) {
                x += addHtmlValue("Nom d'amfitrió", currentMesh.kvm.host);
                x += addHtmlValue("Nom d'usuari", currentMesh.kvm.user);
            }

            // Display device group creator
            if ((currentMesh.creatorid != null) && (users != null) && (users[currentMesh.creatorid] != null)) {
                var meshcreator = users[currentMesh.creatorid];
                x += addHtmlValue("Creador", '<a href=# onclick=gotoUser(\"' + encodeURIComponentEx(meshcreator._id) + '\",false,null)>' + EscapeHtml(meshcreator.name) + '</a>');
            } else if (currentMesh.creatorname != null) {
                x += addHtmlValue("Creador", EscapeHtml(currentMesh.creatorname));
            }

            // Display creation time
            if (currentMesh.creation != null) { x += addHtmlValue("Temps de Creació", printDateTime(new Date(currentMesh.creation))); }

            // Display features
            if (currentMesh.mtype != 3) {
                var meshFeatures = [];
                if (currentMesh.flags) {
                    if (currentMesh.flags & 1) { meshFeatures.push("Eliminació automàtica"); }
                    if (currentMesh.flags & 2) { meshFeatures.push((currentMesh.mtype == 4) ? "Sincronització del nom del port" : "Sincronització del nom d'amfitrió"); }
                    if (currentMesh.flags & 8) { meshFeatures.push("prefereix --agentname"); }
                    if (currentMesh.flags & 16) { meshFeatures.push("permet substituir"); }
                    if ((serverinfo.devGroupSessionRecording == 1) && (currentMesh.flags & 4)) { meshFeatures.push("Sessions de gravació"); }
                }
                if ((typeof currentMesh.expireDevs == 'number') && (currentMesh.expireDevs > 0)) { meshFeatures.push("Elimina inactiu"); }
                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Cap" + '</i>'; }
                x += addHtmlValue("Característiques", 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("Petició de l'escriptori+barra d'eines"); } else if (consent & 0x0040) { meshFeatures.push("Barra d'eines d'escriptori"); } else if (consent & 0x0008) { meshFeatures.push("Petició d'escriptori"); } else { if (consent & 0x0001) { meshFeatures.push("Notificació d'escriptori"); } }
                if (consent & 0x0010) { meshFeatures.push("Indicador del terminal"); } else { if (consent & 0x0002) { meshFeatures.push("Notificació de terminal"); } }
                if (consent & 0x0020) { meshFeatures.push("Solicitud de fitxers"); } else { if (consent & 0x0004) { meshFeatures.push("Notificació de fitxers"); } }
                if (consent == 7) { meshFeatures = ["Notificar sempre"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Sempre prompte"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Cap" + '</i>'; }
                x += addHtmlValue("Consentiment de l'usuari", 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("Connecta't"); }
                    if (meshNotify & 4) { meshNotifyStr.push("Desconnecta"); }
                    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("Correu electrònic ({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("Missatgeria ({0})", xx)); }
                    }
                    if (meshNotifyStr.length == 0) { meshNotifyStr.push('<i>' + "Cap" + '</i>'); }
                    x += addHtmlValue("Notificacions", addLink(meshNotifyStr.join(', '), 'p20editMeshNotify()'));
                }
            }

            // Display invitation codes
            if ((features & 0x01000000) && (currentMesh.mtype == 2)) {
                var inviteCodeStr = '<i>' + "Cap" + '</i>', icodes = false;
                if (currentMesh.invite != null) { icodes = true; inviteCodeStr = currentMesh.invite.codes.join(', '); /* + ', ' + currentMesh.invite.flags;*/ }
                x += addHtmlValue("Codis d'invitació", 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 = "Sense política";
                if (currentMesh.amt) {
                    if (currentMesh.amt.type == 1) { intelAmtPolicy = "Desactivar"; }
                    else if (currentMesh.amt.type == 2) {
                        intelAmtPolicy = "Mode de control de client simple (CCM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 3) {
                        intelAmtPolicy = "Mode de control d'administració simple (ACM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 4) {
                        intelAmtPolicy = "Totalment automàtic";
                    }
                }
                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=' + "Notes" + ' title="' + "Consulta les notes sobre aquest grup de dispositius" + '" 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> ' + "Afegeix usuaris" + '</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> ' + "Afegeix un grup d'usuaris" + '</button>'; }
                }
            }

            //if (meshrights & 4) { }
            if (currentMesh.mtype == 1) {
                if (meshrights & 1) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" title="' + "Importa dispositius 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="' + "Realitzeu l'activació i la configuració d'Intel&reg; AMT." + '" onclick=\'return showAmtSetup("' + currentMesh._id + '")\'><i class="fa-solid fa-gear"></i> ' + "Configuració" + '</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="' + "Afegiu un ordinador nou a aquest grup de dispositius instal·lant l'agent de malla." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Afegeix un agent" + '</button>';
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick=\'return inviteAgentToMesh("' + currentMesh._id + '")\' title="' + "Convida algú a instal·lar l'agent de malla en aquest grup de dispositius." + '"><i class="fa-solid fa-user-plus"></i> ' + "Convidar" + '</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="' + "Afegeix un dispositiu situat a la xarxa local." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Afegeix un dispositiu" + '</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="' + "Canvia Intel AMT al mode de control d'administració (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>' + "Autoritzacions d'usuari" + '</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="' + "Elimina els drets d'usuari d'aquest grup de dispositius" + '"><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="' + "Usuari" + '" 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>' + "Compartició activa de dispositius" + '</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="' + "Enllaç per compartir dispositiu" + '" 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="' + "Elimina l'ús compartit de dispositius" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                    var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Escriptori", "Escriptori + Terminal", "Fitxers", "Terminal + Fitxers", "Escriptori + Fitxers", "Escriptori + Terminal + Fitxers"][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} durant {2} minut", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        } else {
                            details = format("{0}, {1} durant {2} minuts", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        }
                    }
                    if (dshare.recurring == 1) { details += ", Recurrent diàriament"; }
                    if (dshare.recurring == 2) { details += ", Recurrent setmanal"; }
                    if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Visualitza només l'escriptori"; }
                    if (dshare.consent != null) {
                        if (dshare.consent == 0) { details += ", Sense consentiment"; } else {
                            if ((dshare.consent & 0x0038) != 0) { details += ", Sol·licitar el consentiment"; }
                            if ((dshare.consent & 0x0040) != 0) { details += ", Barra d'eines"; }
                        }
                    }
                    var guestName = EscapeHtml(dshare.guestName);
                    if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Autocompartició de l'agent" + '</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="' + "Descarrega la llista de dispositius" + '" src="images/link4.png" /></a>';
                if ((features & 1) == 0) { dllist += ' <a onclick=meshImportDeviceList()><img title="' + "Importa la llista de dispositius" + '" 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>' + "Dispositius" + '</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>' + "Cap" + '</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>' + "Suprimeix el grup" + '</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 = "Importeu una llista de dispositius locals Intel&reg; AMT en format MeshCommander JSON." + '<br /><br /><input type=file id=d4importFile class="form-control" accept=".json" onchange=meshImportDeviceListValidate() />';
            setModalContent('xxAddAgent', "Importa dispositius 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', "Importa dispositius Intel&reg; AMT", format("Fitxer JSON no vàlid: {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', "Importació del compte d'usuari", 'Unable to import any device.');
                    }
                } else {
                    setModalContent('xxAddAgent', "Importa dispositius 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>' + "Mode de control d'administració simple (ACM)" + '</option>'; }
            x += addHtmlFormFloating("Tipus", '<select id=dp20amtpolicy class="form-select" onchange=p20editMeshAmtChange()><option value=0>' + "Sense política" + '</option><option value=1>' + "Desactivar" + '</option><option value=2>' + "Mode de control de client simple (CCM)" + '</option>' + acmoption + '<option value=4>' + "Totalment automàtic" + '</option></select>')
            x += '<div id=dp20amtpolicydiv></div>';

            setModalContent('xxAddAgent', "Política 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>' + "Mantenir la contrasenya existent" + '</option>' : '';
                x += addHtmlFormFloating("Contrasenya", '<select id=dp20amtpass class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Contrasenya aleatòria" + '</option>' + keeppass + '<option value=2>' + "Seleccioneu una nova contrasenya" + '</option></select>')
                x += '<div id=dp20amtpassdiv style=display:none>';
                x += addHtmlFormFloating("Nova contrasenya*", '<input id=dp20amtpolicypass type=password class="form-control" maxlength=32 placeholder="New password*" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy() autocomplete=off />');
                x += addHtmlFormFloating("Nova contrasenya*", '<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("Mode CCM", '<select id=dp20amtccmmode class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "No canvieu, manteniu CCM si està configurat" + '</option><option value=1>' + "Desactiveu CCM si està configurat" + '</option><option value=2>' + "Activa a CCM, si falla l'ACM" + '</option></select>'); }
                x += '<div id=dp20amtbadpassdiv style=display:none>';
                x += addHtmlFormFloating("Contrasenya desconeguda", '<select id=dp20amtbadpass class="form-select"><option value=0>' + "No fer res" + '</option><option value=1>' + "Si estàs a CCM, reactiva Intel&reg; AMT" + '</option></select>');
                x += '</div>';
                if ((features & 0x400) == 0) { x += addHtmlFormFloating("Configuració CIRA", '<select id=dp20amtcira class="form-select" title="' + "Accés remot iniciat pel client" + '"><option value=0>' + "No fer res" + '</option><option value=1>' + "No connecteu al servidor" + '</option><option value=2>' + "Connecteu-vos al servidor" + '</option></select>'); }
                x += '<span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 caràcters, 1 superior, 1 inferior, 1 numèric, 1 no alfanumèric." + '</span>';
                if ((currentMesh.mtype == 2) && (ptype == 2)) { x += '<span style="font-size:10px"> ' + "Aquesta política no afectarà els dispositius amb Intel&reg; AMT en mode 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">' + "Quan se selecciona aquesta política, aquest servidor no gestiona Intel&reg; AMT. Intel AMT encara es pot utilitzar activant-lo i configurant-lo manualment." + '</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">' + "Quan se selecciona aquesta política, es desactivarà qualsevol Intel&reg; AMT en mode de control de client (CCM). Altres dispositius tindran CIRA esborrat i encara es poden gestionar manualment." + '</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">' + "Aquesta és la política recomanada. L'activació i la gestió d'Intel&reg; AMT estan completament automatitzades i el servidor intentarà fer el millor ús possible de la gestió del maquinari." + '</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("Esteu segur que voleu suprimir el grup {0}? En suprimir el grup de dispositius, també se suprimirà tota la informació sobre els dispositius d'aquest grup.", EscapeHtml(currentMesh.name)) + '<br /><br />';
            x += '<label class=form-check-label><input id=p20check type=checkbox class="form-check-input m-2" onchange=p20validateDeleteMeshDialog() />' + "Confirmeu" + '</label>';
            setModalContent('xxAddAgent', "Suprimeix el grup", 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 hi ha cap dispositiu de relé disponible.";
                setModalContent('xxAddAgent', "Edita el grup de dispositius", "No hi ha cap dispositiu de relé disponible.");
                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("Dispositiu de relé", '<select id=d2devrelay class="form-select">' + relayDevices2.join('') + '</select>');
                setModalContent('xxAddAgent', "Edita el grup de dispositius", 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="' + "Nom" + '" onchange=p20editmeshValidate() onkeyup=p20editmeshValidate(event) />';
            x += '<label for=dp20meshname>' + "Nom" + '</label>';
            x += '</div>';

            x += '<div class="form-floating mb-2">';
            x += '<textarea id=dp20meshdesc class="form-control" maxlength=1024 placeholder="' + "Descripció" + '"></textarea>';
            x += '<label for=dp20meshdesc>' + "Descripció" + '</label>';
            x += '</div>';
            setModalContent('xxAddAgent', "Edita el grup de dispositius", 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 = "Edita el consentiment de l'usuari del grup de dispositius"; }
            if (editType == 2) { consent = (currentUser.consent) ? currentUser.consent : 0; title = "Edita el consentiment de l'usuari"; }
            if (editType == 3) { consent = (currentNode.consent) ? currentNode.consent : 0; title = "Edita el consentiment de l'usuari del dispositiu"; }
            if (editType == 4) { consent = (currentUserGroup.consent) ? currentUserGroup.consent : 0; title = "Edita el consentiment d'usuari del grup d'usuaris"; }
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px"><b>' + "Escriptori" + '</b></div>';
            x += '<div><label for=d20flag1><input type=checkbox id=d20flag1 class="form-check-input me-2" ' + ((consent & 0x0001) ? 'checked' : '') + '>' + "Notificar a l'usuari" + '</label></div>';
            x += '<div><label for=d20flag2><input type=checkbox id=d20flag2 class="form-check-input me-2" ' + ((consent & 0x0008) ? 'checked' : '') + '>' + "Sol·licitar el consentiment de l'usuari" + '</label></div>';
            x += '<div><label for=d20flag7><input type=checkbox id=d20flag7 class="form-check-input me-2" ' + ((consent & 0x0040) ? 'checked' : '') + '>' + "Mostra la barra d'eines de connexió" + '</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 a l'usuari" + '</label></div>';
            x += '<div><label for=d20flag4><input type=checkbox id=d20flag4 class="form-check-input me-2" ' + ((consent & 0x0010) ? 'checked' : '') + '>' + "Sol·licitar el consentiment de l'usuari" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Fitxers" + '</b></div>';
            x += '<div><label for=d20flag5><input type=checkbox id=d20flag5 class="form-check-input me-2" ' + ((consent & 0x0004) ? 'checked' : '') + '>' + "Notificar a l'usuari" + '</label></div>';
            x += '<div><label for=d20flag6><input type=checkbox id=d20flag6 class="form-check-input me-2" ' + ((consent & 0x0020) ? 'checked' : '') + '>' + "Sol·licitar el consentiment de l'usuari" + '</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' : '') + '>' + "Gravar sessions" + '</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) ? "Sincronitza el nom del dispositiu del servidor amb el nom del port" : "Sincronitza el nom del dispositiu del servidor amb el nom de l'amfitrió") + '</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' : '') + '>' + "Prefereix 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' : '') + '>' + "Permet substituir el nom del dispositiu del servidor fins a la pròxima connexió" + '</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' : '') + '>' + "Elimina el dispositiu en desconnectar" + '</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' : '') + '>' + "Elimina automàticament els dispositius inactius" + '</label><br></div>';
            x += '<div style=margin-left:20px id=d20expireDeviceDev>' + "Desactiva els dies fins a l'eliminació" + ' <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 manera predeterminada, els dispositius inactius s'eliminaran al cap d'1 dia." + '</span>', serverinfo.autoremoveinactivedevices);
                } else {
                    x += format('<span style=font-size:x-small;margin-left:6px>' + "De manera predeterminada, els dispositius inactius s'eliminaran al cap de {0} dies." + '</span>', serverinfo.autoremoveinactivedevices);
                }
            }

            setModalContent('xxAddAgent', "Edita les funcions del grup de dispositius", 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 += "Permet als usuaris gestionar aquest grup de dispositius i els dispositius d'aquest grup."; } else { x += "Permet als usuaris gestionar aquest dispositiu."; }
                    if (features & 0x00080000) { x += " Els usuaris han d'iniciar sessió a aquest servidor una vegada abans de poder afegir-se a un grup de dispositius." }
                    x += '<br /><br />';
                }
                x += '<div style=\'position:relative\'>';
                x += addHtmlFormFloating("Identificadors d'usuari", '<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("Grup de dispositius", '<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("Grup d'usuaris", '<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("Grup d'usuaris", '<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("Grup de dispositius", '<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("Grup de dispositius", '<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("Dispositiu", '<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 grup per a {0}.", uname) + '<br /><br />';
                } else {
                    x += format("Permisos de grup per a l'usuari {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>' + "Edita el grup de dispositius" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20manageusers>' + "Gestioneu els usuaris del grup de dispositius" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20managecomputers>' + "Gestiona els ordinadors del grup de dispositius" + '</label><br>';
            }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotecontrol>' + "Control remot i relé" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remoteview style=margin-left:12px>' + "Només visualització remota" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotelimitedinput style=margin-left:12px>' + "Només 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>' + "Compartició de convidats" + '</label><br>'; }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nodesktop style=margin-left:12px>' + "Sense accés a l'escriptori" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noterminal style=margin-left:12px>' + "Sense accés al terminal" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nofiles style=margin-left:12px>' + "Sense accés a fitxers" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noamt style=margin-left:12px>' + "No Intel&reg; AMT" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshagentconsole>' + "Consola de l'agent de malla" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshserverfiles>' + "Fitxers del servidor" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20wakedevices>' + "Dispositius d'activació" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editnotes>' + "Edita les notes del dispositiu" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20limitevents>' + "Mostra només esdeveniments propis" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20chatnotify>' + "Xateja i notifica" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20uninstall>' + "Desinstal·leu l'agent/elimineu el dispositiu" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20commands>' + "Comandaments a distància" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20resetoff>' + "Restablir / Apagar" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20details>' + "Detalls del dispositiu" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20relay>' + "Utilitzar com a relé" + '</label><br>';
            x += '</div>';

            if (userid == null) {
                xxdialogButtons = 3; xxdialogTag = null;
                setModalContent('xxAddAgent', "Afegeix usuaris al grup de dispositius", 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) ? "Afegeix permisos de grup de dispositius" : "Edita els permisos del grup de dispositius", 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', "Afegeix un grup d'usuaris", 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) ? "Afegeix un grup de dispositius" : "Edita el grup de dispositius", 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) ? "Afegeix permisos del dispositiu" : "Edita els permisos del dispositiu", 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) ? "Afegeix permisos del dispositiu" : "Edita els permisos del dispositiu", 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 ? "Edita els permisos del dispositiu d'usuari" : "Afegeix permisos de dispositiu d'usuari", 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 ? "Edita els permisos del dispositiu del grup d'usuaris" : "Afegeix permisos de dispositiu de grup d'usuaris", 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', "Edita els permisos del grup de dispositius", x);
                } else {
                    xxdialogButtons = 7, xxdialogTag = userid;
                    setModalContent('xxAddAgent', "Edita els permisos del grup de dispositius d'usuari", 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 (tots els drets)"); else {
                    if ((meshrights & 1) != 0) r.push("Edita el grup de dispositius");
                    if ((meshrights & 2) != 0) r.push("Gestioneu els usuaris del grup de dispositius");
                    if ((meshrights & 4) != 0) r.push("Gestiona els ordinadors del grup de dispositius");
                    if ((meshrights & 8) != 0) r.push("Control remot");
                    if ((meshrights & 16) != 0) r.push("Consola de l'agent");
                    if ((meshrights & 32) != 0) r.push("Fitxers del servidor");
                    if ((meshrights & 64) != 0) r.push("Dispositius d'activació");
                    if ((meshrights & 128) != 0) r.push("Edita les notes");
                    if (((meshrights & 8) != 0) && (meshrights & 256) != 0) r.push("Només visualització remota");
                    if (((meshrights & 8) != 0) && (meshrights & 65536) != 0) r.push("Sense escriptori");
                    if (((meshrights & 8) != 0) && (meshrights & 512) != 0) r.push("Sense terminal");
                    if (((meshrights & 8) != 0) && (meshrights & 1024) != 0) r.push("No hi ha fitxers");
                    if (((meshrights & 8) != 0) && (meshrights & 2048) != 0) r.push("No Intel&reg; AMT");
                    if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("Entrada limitada");
                    if ((meshrights & 8192) != 0) r.push("Només esdeveniments propis");
                    if ((meshrights & 16384) != 0) r.push("Xateja i notifica");
                    if ((meshrights & 32768) != 0) r.push("Desinstal·la");
                    if ((meshrights & 131072) != 0) r.push("Ordres");
                    if ((meshrights & 262144) != 0) r.push("Restablir/Desactivar");
                    if ((meshrights & 524288) != 0) r.push("Compartint");
                    if ((meshrights & 1048576) != 0) r.push("Detalls");
                    if ((meshrights & 2097152) != 0) r.push("Relleu");
                }
                if (r.length == 0) { r.push("Sense Drets"); }
                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("Nom d'usuari", EscapeHtml(decodeURIComponent(uname)));
                if (xuserid.split('/')[2] != uname) { x += addHtmlValue("Identificador d'usuari", EscapeHtml(xuserid.split('/')[2])); }

                x += addHtmlValue("Permisos", r.join(","));
                if (((userinfo._id) != xuserid) && (cmeshrights == 0xFFFFFFFF || (((cmeshrights & 2) != 0) && (meshrights != 0xFFFFFFFF)))) buttons += 4;
                setModalContent('xxAddAgent', "Usuari del grup de dispositius", 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', "Elimina els permisos d'usuari", format("Confirmeu l'eliminació dels drets de l'usuari \"{0}\"?", EscapeHtml(decodeURIComponent(uname))));
            }
            if (userid.startsWith('ugrp/')) {
                setModalContent('xxAddAgent', "Elimina els permisos del grup d'usuaris", format("Confirmeu l'eliminació dels drets per al grup d'usuaris \"{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 = "Quan està activat, qualsevol persona pot utilitzar els codis d'invitació per unir dispositius a aquest grup de dispositius mitjançant l'enllaç públic següent:" + '<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() />' + "Activa els codis d'invitació" + '</label></div>';
                x += addHtmlFormFloating("Codis d'invitació", '<input id=agentInviteCode class="form-control" onkeyup=p20editmeshInviteCodeValidate() placeholder="code1, code2, code3" />');
                x += addHtmlFormFloating("Agents", '<select id=agentType class="form-select" onchange=p20editmeshInviteCodeValidate()><option value=0>' + "Tots els agents disponibles" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "Assistent MeshCentral" + '</option></select>');
                x += '<div id=d2agentInstallTypeDiv>';
                x += addHtmlFormFloating("Tipus d'instal·lació", '<select id=agentInviteType class="form-select"><option value=0>' + "Fons i interactiu" + '</option><option value=2>' + "Només fons" + '</option><option value=1>' + "Només interactiu" + '</option></select>');
                x += '</div>';
                setModalContent('xxAddAgent', "Codis d'invitació", 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 = "Qualsevol persona pot utilitzar els codis d'invitació per unir dispositius a aquest grup de dispositius mitjançant l'enllaç públic següent:" + '<br /><br />';
                x += '<div style=width:100%;text-align:center><a rel="noreferrer noopener" target=_blank href="' + url + '">' + url + '</a></div><br />';
                x += addHtmlValue("Codis d'invitació", currentMesh.invite.codes.join(', '));
                x += addHtmlValue("Tipus d'instal·lació", ["Fons i interactiu", "Només interactiu", "Només fons"][currentMesh.invite.flags & 3]);
                setModalContent('xxAddAgent', "Codis d'invitació", 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">' + "Notificacions de pàgines web" + '</div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Connexions de dispositius" + '</label></div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconnexions del dispositiu" + '</label></div>';
            if (currentMesh.mtype < 3) {
                x += '<div class="form-check"><label><input id=p20notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Esdeveniments d'escriptori i sèrie Intel&reg; AMT" + '</label></div>';
            }
            if (emailNotify) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificacions per correu electrònic" + '</div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Connexions de dispositius" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconnexions del dispositiu" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceHelp class="form-check-input me-2" type=checkbox />' + "Sol·licituds d'ajuda" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Notificacions de missatgeria" + '</div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceConnect class="form-check-input me-2" type=checkbox />' + "Connexions de dispositius" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Desconnexions del dispositiu" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceHelp class="form-check-input me-2" type=checkbox />' + "Sol·licituds d'ajuda" + '</label></div>';
            }

            setModalContent('xxAddAgent', "Configuració de notificacions", 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 connectat", "Agent", "Intel&reg; AMT", "Agent + 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: ["D'acord", "Antivirus no actiu", "No hi ha actualització automàtica", "El tallafoc no està actiu", "Múltiples Problemes"] },
                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>' + "Cap" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span tabindex=0 title="' + "Feu clic aquí per editar el nom del grup de dispositius" + '" 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>' + "Estats de poder" + '</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>' + "Tipus d'agents" + '</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 connectat", "Agent", "Intel&reg; AMT", "Agent + 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>' + "Connectivitat" + '</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 /> ' + "D'acord",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noav")\' width=10 height=10 /> ' + "Antivirus no actiu",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noupdate")\' width=10 height=10 /> ' + "No hi ha actualització automàtica",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:nofirewall")\' width=10 height=10 /> ' + "El tallafoc no està actiu",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:any")\' width=10 height=10 /> ' + "Múltiples Problemes"
                    ][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>' + "Seguretat" + '</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 hi ha dispositius en aquest grup de dispositius." + '</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)">' + "Arrel" + '</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="' + "Mostra l'enllaç públic" + '" onclick=\'return p5showPublicLink("' + (publicPath + '/' + encodeURIComponentEx(f.nx)) + '?download=1' + loginkey + '")\'></i>';
                        publiclink += '<i class="fa-solid fa-download fa-fw" title="' + "Copia l'enllaç al porta-retalls" + '" 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ímit d'emmagatzematge supera";
            if (bytes < 2048) return format("Queden {0} bytes", bytes);
            if (bytes < 2097152) return format("Queden {0} kilobytes", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("Queden {0} megabytes", Math.round(bytes / 1024 / 1024));
            return format("Queden {0} gigabytes", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize2(bytes) {
            if (bytes <= 0) return "Cap";
            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 "Cap";
            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} fitxers. {2}k com a màxim", tf, f.c, (Math.floor(f.maxbytes / 1024 / 1024))); } else { title = format("{0}k en 1 fitxer. {1}k com a màxim", 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', "Enllaç públic", '<input type=text style=width:350px value="' + u + '" readonly /> <i class="fa-regular fa-copy" title="' + "Copia l'enllaç al porta-retalls" + '" 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 ? "Seleccioneu Cap" : "Seleccionar tot");
            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', "Carpeta nova", '<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>' + "Eliminació recursiva" + '</label><br>' : '<input type=checkbox id=p5recdeleteinput class="form-check-input me-2" style=\'display:none\'>';
            setModalContent('xxAddAgent', "Suprimeix", (cc > 1) ? (format("Vols suprimir {0} elements seleccionats?", cc) + rec) : ("Vols suprimir l'element seleccionat?" + 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', "Canvia el nom", '<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', "Penja el document", '<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\')" />' + "Vols confirmar la sobreescritura?" + '</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 fitxers", "Només es poden editar fitxers de menys de 200 k."); }
                    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("Confirmeu {0} de {1} entrada{2} a aquesta ubicació?", (p5clipboardCut == 0 ? 'copy' : 'move'), p5clipboard.length, ((p5clipboard.length > 1) ? 's' : '')) }
            setModalContent('xxAddAgent', "Enganxa", 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("Mantingut {0} entrada{1} per a {2}", p5clipboard.length, ((p5clipboard.length > 1) ? 's' : ''), (p5clipboardCut == 0 ? "Còpia" : "Move")) + ', <a href=# onclick="return p5clearClip()" style=cursor:pointer>' + "Clar" + '</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', "Penja el document", format((overWriteCount == 1) ? "La càrrega sobreescriurà 1 fitxer. Continuar?" : "La càrrega sobreescriurà {0} fitxers. 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: "Inici de sessió al compte",
            2: "Tancament del compte",
            3: "S'ha canviat l'idioma de {1} a {2}",
            4: "S'ha unit a la sessió multiplex d'escriptori", // No longer in use, replaced with 143
            5: "Va sortir de la sessió múltiplex d'escriptori", // No longer in use, replaced with 144
            6: "S'ha iniciat la sessió múltiplex d'escriptori", // No longer in use, replaced with 145
            7: "Sessió de gravació acabada, {0} segons(s)", // No longer in use, replaced with 146
            8: "Sessió múltiplex d'escriptori tancada, {0} segons", // No longer in use, replaced with 147
            9: "S'ha acabat la sessió de relleu \"{0}\" de {1} a {2}, {3} segons(s)",
            10: "S'ha acabat la sessió del terminal \"{0}\" de {1} a {2}, {3} segons(s)",
            11: "S'ha acabat la sessió d'escriptori \"{0}\" de {1} a {2}, {3} segons(s)",
            12: "S'ha finalitzat la sessió de gestió de fitxers \"{0}\" de {1} a {2}, {3} segons(s)",
            13: "S'ha iniciat la sessió de retransmissió \"{0}\" de {1} a {2}",
            14: "S'ha iniciat la sessió del terminal \"{0}\" de {1} a {2}",
            15: "S'ha iniciat la sessió d'escriptori \"{0}\" de {1} a {2}",
            16: "S'ha iniciat la sessió de gestió de fitxers \"{0}\" de {1} a {2}",
            17: "S'està processant l'ordre de la consola: \"{0}\"",
            18: "S'està mostrant el quadre de missatge, title=\"{0}\", message=\"{1}\"",
            19: "Procés de matança {0} ({1})",
            20: "Obertura: {0}",
            21: "S'està obtenint contingut del porta-retalls, {0} bytes",
            22: "Configuració del contingut del porta-retalls, {0} bytes",
            23: "S'està intentant activar el mode Intel(R) AMT ACM",
            24: "Execució d'ordres",
            25: "Realitzant una acció de poder={0}, forçat={1}",
            26: "S'està mostrant el missatge de brindis, title=\"{0}\", message=\"{1}\"",
            27: "L'usuari local ha acceptat la sol·licitud del terminal remot",
            28: "L'usuari local ha rebutjat la sol·licitud del terminal remot",
            29: "Connexió a l'escriptori remot tancada força per l'usuari local",
            30: "S'està iniciant l'escriptori remot després que l'usuari local hagi acceptat",
            31: "Barra de connexió a l'escriptori remot activada/actualitzada",
            32: "La barra de connexió a l'escriptori remot ha fallat o no és compatible",
            33: "Connexió a l'escriptori remot tancada força per l'usuari local", // No longer used, 29 must be used instead.
            34: "No s'ha pogut iniciar l'escriptori remot després de rebutjar l'usuari local",
            35: "S'ha iniciat l'escriptori remot amb notificació de brindis",
            36: "S'ha iniciat l'escriptori remot sense notificació",
            37: "Barra de connexió a l'escriptori remot activada/actualitzada", // No longer used, 31 must be used instead.
            38: "La barra de connexió a l'escriptori remot ha fallat o no és compatible", // No longer used, 33 must be used instead.
            39: "Connexió a l'escriptori remot tancada força per l'usuari local", // No longer used, 29 must be used instead.
            40: "S'està iniciant fitxers remots després que l'usuari local hagi acceptat",
            41: "No s'han pogut iniciar els fitxers remots després de rebutjar l'usuari local",
            42: "S'han iniciat fitxers remots amb notificació de brindis",
            43: "S'han iniciat fitxers remots sense notificació",
            44: "Crea una carpeta: \"{0}\"",
            45: "Suprimeix: \"{0}\"",
            46: "Suprimeix recursiu: \"{0}\", {1} elements eliminats",
            47: "Suprimeix: \"{0}\", {1} elements eliminats",
            48: "Canvia el nom: \"{0}\" a \"{1}\"",
            49: "Descarrega: \"{0}\"",
            50: "Carrega: \"{0}\"",
            51: "Copia: \"{0}\" a \"{1}\"",
            52: "Mou: \"{0}\" a \"{1}\"",
            53: "Bloqueig de l'usuari remot fora de l'escriptori",
            54: "L'agent ha tancat la sessió amb un {0}% de compressió d'agent a servidor. Enviat: {1}, comprimit: {2}",
            55: "Grup de dispositius creat: {0}",
            56: "S'ha recuperat el grup de dispositius: {0}",
            57: "S'ha afegit el dispositiu {0} al grup de dispositius {1}",
            58: "El dispositiu ha sol·licitat l'activació d'Intel(R) AMT ACM, FQDN: {0}",
            59: "S'ha canviat el dispositiu {0} del grup {1}: {2}",
            60: "S'han eliminat els drets del dispositiu d'usuari per a {0}",
            61: "S'han canviat els drets del dispositiu d'usuari per a {0}",
            62: "S'ha eliminat l'usuari {0} del grup d'usuaris {1}",
            63: "S'ha eliminat el compte",
            64: "Compte creat, el nom d'usuari és {0}",
            65: "S'ha creat el compte, el correu electrònic és {0}",
            66: "Compte canviat: {0}",
            67: "La pertinença al grup d'usuaris ha canviat: {0}",
            68: "S'ha afegit el grup d'usuaris {0} al grup de dispositius {1}",
            69: "Grup d'usuaris creat: {0}",
            70: "S'ha eliminat el grup d'usuaris {0} del grup de dispositius {1}",
            71: "S'ha afegit l'usuari {0} al grup d'usuaris {1}",
            72: "S'ha eliminat l'usuari {0} del grup d'usuaris {1}",
            73: "La notificació del grup de dispositius ha canviat",
            74: "La contrasenya del compte ha canviat: {0}",
            75: "S'han canviat les credencials del compte",
            76: "Grup de dispositius creat: {0}",
            77: "Grup de dispositius suprimit: {0}",
            78: "La pertinença al grup de dispositius ha canviat: {0}",
            79: "El grup d'usuaris ha canviat: {0}",
            80: "S'ha afegit l'usuari {0} al grup d'usuaris {1}",
            81: "S'han eliminat els drets del dispositiu d'usuari per a {0}",
            82: "S'han canviat els drets del dispositiu d'usuari per a {0}",
            83: "S'ha eliminat l'usuari {0} del grup de dispositius {1}",
            84: "S'ha afegit el dispositiu {0} al grup de dispositius {1}",
            85: "S'ha mogut el dispositiu {0} al grup {1}",
            86: "S'han eliminat els drets del dispositiu d'usuari per a {0}",
            87: "S'ha eliminat el dispositiu {0} del grup de dispositius {1}",
            88: "S'ha activat l'autenticació de dos factors de correu electrònic",
            89: "S'ha desactivat l'autenticació de dos factors de correu electrònic",
            90: "S'ha afegit una aplicació d'autenticació",
            91: "S'ha eliminat l'aplicació d'autenticació",
            92: "S'han generat nous codis de seguretat 2FA",
            93: "S'han esborrat els codis de seguretat 2FA",
            94: "S'ha eliminat la clau de seguretat",
            95: "S'ha afegit clau de seguretat",
            96: "Número de telèfon verificat de l'usuari {0}",
            97: "S'ha eliminat el número de telèfon de l'usuari {0}",
            98: "Ajuda sol·licitada, usuari: {0}, detalls: {1}",
            99: "Execució d'ordres com a usuari",
            100: "Si és possible executar ordres com a usuari",
            101: "S'ha afegit el dispositiu compartit {0} de {1} a {2}",
            102: "S'ha eliminat el dispositiu compartit {0}",
            103: "Càrrega per lots de {0} fitxers a la carpeta {1}",
            104: "Descàrrega automàtica del fitxer d'abocament bàsic de l'agent: \"{0}\"",
            105: "Carrega: \"{0}\", Mida: {1}",
            106: "Baixa: \"{0}\", Mida: {1}",
            107: "Inici de sessió al compte des de {0}, {1}, {2}",
            108: "Intent d'inici de sessió d'usuari amb un segon factor incorrecte de {0}, {1}, {2}",
            109: "Intent d'inici de sessió d'usuari al compte bloquejat de {0}, {1}, {2}",
            110: "Intent d'inici de sessió d'usuari no vàlid de {0}, {1}, {2}",
            111: "El dispositiu ha sol·licitat l'activació TLS d'Intel(R) AMT ACM, FQDN: {0}",
            112: "S'ha acabat la sessió de missatgeria \"{0}\" de {1} a {2}, {3} segons(s)",
            113: "S'ha afegit un dispositiu d'autenticació de notificacions push",
            114: "S'ha eliminat el dispositiu d'autenticació de notificacions push",
            115: "S'ha afegit un testimoni d'inici de sessió",
            116: "S'ha eliminat el testimoni d'inici de sessió",
            117: "Aquesta és una versió antiga de l'agent, considereu actualitzar-la.",
            118: "Aquest agent té un mecanisme de validació de certificats obsolet, considereu actualitzar-lo.",
            119: "Aquest agent està utilitzant túnels insegurs, considereu actualitzar-lo.",
            120: "S'ha iniciat la sessió de retransmissió local \"{0}\", protocol {1} ​​a {2}",
            121: "S'ha acabat la sessió de retransmissió local \"{0}\", protocol {1} ​​a {2}, {3} segons(s)",
            122: "Va sortir de la sessió múltiplex d'escriptori al cap de {0} segons.", // No longer in use, replaced with 144
            123: "Va sortir la sessió Web-SSH \"{1}\" al cap de {0} segons.",
            124: "Va sortir la sessió de Web-SFTP \"{1}\" al cap de {0} segons.",
            125: "Va sortir la sessió Web-RDP \"{1}\" al cap de {0} segons.",
            126: "Va sortir de la sessió Web-VNC al cap de {0} segons.",
            127: "S'ha canviat el nom visible del compte a {0}.",
            128: "S'ha creat el compte, el nom és {0}.",
            129: "S'ha eliminat el nom de visualització del compte.",
            130: "Les notificacions dels usuaris han canviat",
            131: "S'ha afegit el dispositiu compartit {0} amb temps il·limitat.",
            132: "Encendre.",
            133: "Tanca.",
            134: "Sessió d'escriptori desconnectada per la força de l'usuari {0}",
            135: "Sessió de terminal desconnectada per la força de l'usuari {0}",
            136: "Sessió de fitxers desconnectats per la força de l'usuari {0}",
            137: "Sessió d'encaminament desconnectada per la força de l'usuari {0}",
            138: "S'ha afegit el dispositiu compartit {0} diàriament.",
            139: "S'ha afegit el dispositiu compartit {0} recurrent setmanalment.",
            140: "S'ha canviat el dispositiu {0} del grup {1}: {2}",
            141: "Canvi de política d'Intel(r) AMT",
            142: "S'ha canviat el grup de dispositius {0}: {1}",
            143: "S'ha unit a la sessió múltiplex d'escriptori \"{0}\"",
            144: "Va sortir de la sessió múltiplex d'escriptori \"{0}\" al cap de {1} segons.",
            145: "S'ha iniciat la sessió múltiplex d'escriptori \"{0}\"",
            146: "S'ha acabat la sessió d'enregistrament \"{0}\", {1} segons(s)",
            147: "Sessió múltiplex d'escriptori tancada \"{0}\", {1} segons(s)",
            148: "S'ha iniciat la sessió Web-SSH \"{0}\".",
            149: "S'ha iniciat la sessió Web-SFTP \"{0}\".",
            150: "S'ha iniciat la sessió Web-RDP \"{0}\".",
            151: "S'ha iniciat la sessió Web-VNC \"{0}\".", // Not in use yet
            152: "Ja no és un relé per a \"{0}\".",
            153: "És un relé per a \"{0}\".",
            154: "El compte ha canviat per sincronitzar-se amb les dades LDAP.",
            155: "S'ha denegat l'inici de sessió d'usuari de {0}, {1}, {2}",
            156: "Compte de missatgeria verificat de l'usuari {0}",
            157: "S'ha eliminat el compte de missatgeria de l'usuari {0}",
            158: "S'està mostrant el quadre d'alerta, title=\"{0}\", message=\"{1}\"",
            159: "Dispositiu encès",
            160: "Enabled Duo two-factor authentication",
            161: "Disabled Duo two-factor authentication",
            162: "S'ha iniciat la sessió de missatgeria \"{0}\" de {1} a {2}",
            163: "S'ha eliminat el dispositiu {0} del grup d'usuaris {1}",
            164: "Crea fitxer: \"{0}\""
        };

        var eventsShortMessageId = {
            1: "Nom del grup",
            2: "Descripció",
            3: "Banderes",
            4: "Consentiment",
            5: "Eliminació automàtica",
            6: "Codi d'invitació",
            7: "Dispositiu de relé"
        }

        // 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="' + "Aquesta és una sessió d'intercanvi de convidats" + '">' + 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 s'han trobat esdeveniments" + '</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("Tipus d'informe", '<select id=d2reportType class="form-select" onchange=p3showReportValidation()><option value=1>' + "Tots els Esdeveniments" + '</option>');
            x += addHtmlFormFloating("Lapse de temps", '<select id=d2reportSpan class="form-select" onchange=p3showReportValidation()><option value=1>' + "Tot disponible" + '</option><option value=2>' + "Un dia" + '</option></select>');
            x += '<div id=d2daySelector>' + addHtmlFormFloating("Dia de l'Informe", '<input type=date id=d2reportDay class="form-control" onchange=p3showReportValidation()></input>') + '</div>';
            x += addHtmlFormFloating("Agrupar per", '<select id=d2reportGroupBy class="form-select" onchange=p3showReportValidation()><option value=1>' + "Cap" + '</option><option value=2>' + "Grup de dispositius" + '</option><option value=3>' + "Usuari" + '</option>');
            x += addHtmlFormFloating("Format", '<select id=d2reportFormat class="form-select" onchange=p3showReportValidation()><option value=1>' + "CSV" + '</option><option value=2>' + "JSON" + '</option>');
            x += '</div>';
            setModalContent('xxAddAgent', "Descarrega l'informe", 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 = "Descarrega la llista d'esdeveniments amb un dels formats de fitxer següents." + '<br /><br />';
            x += addHtmlValue("Format CSV", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogCSV(' + mode + ')">' + 'eventslist.csv' + '</a>');
            x += addHtmlValue("Format JSON", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogJSON(' + mode + ')">' + 'eventslist.json' + '</a>');
            setModalContent('xxAddAgent', "Exportació de la llista d'esdeveniments", 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, hora, tipus, acció, usuari, dispositiu, missatge" + '\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>' + "Nom" + '<th style=width:80px>' + "Grups de dispositius" + '<th style=width:120px>' + nobreak("Últim accés") + '<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>' + "Usuaris en línia"; 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>' + "Usuaris fora de línia"; addHeader = false; }
                        x += addUserHtml(user, sessions);
                        maxUsers--;
                    } else {
                        hiddenUsers++;
                    }
                }
            }
            x += '</table>';
            if (hiddenUsers == 1) { x += '<br />' + "No es mostra 1 usuari més, utilitzeu el quadre de cerca per cercar usuaris..." + '<br />'; }
            else if (hiddenUsers > 1) { x += '<br />' + format("No es mostren {0} usuaris més, utilitzeu el quadre de cerca per cercar usuaris...", hiddenUsers) + '<br />'; }
            if (maxUsers == displayUsersLimit) { x += '<br />' + "No s'han trobat usuaris." + '<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=' + "Xatejar" + '><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 sessió"); } else { lastAccess += nobreak(format("{0} sessions", sessions)); }
            } else {
                if (user.access) { lastAccess += '<span title="' + format("Últim accés: {0}", printDateTime(new Date(user.access * 1000))) + '">' + printDate(new Date(user.access * 1000)) + '</span>'; }
                else if (user.login) { lastAccess += '<span title="' + format("Darrer inici de sessió: {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 += "Tancat" + ',&nbsp;'; }
            permissions += '<span title=\'' + "Permisos del servidor" + '\'>';

            var urights = user.siteadmin & (0xFFFFFFFF - 1248);
            if ((user.siteadmin == null) || (urights == 0)) {
                permissions += "Usuari";
            } else if (urights == 8) {
                permissions += "Usuari + Fitxers";
            } else if (user.siteadmin == 0xFFFFFFFF) {
                permissions += "Administrador";
            } else if ((urights & 2) != 0) {
                permissions += "Gerent";
            } 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="' + "El correu electrònic no està verificat" + '">&#x2717;</b>' : ' <b style=color:green title="' + "El correu electrònic està verificat" + '">&#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 correu electrònic a l'usuari" + '" 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="' + "S'ha activat l'autenticació del segon factor" + '" style="margin-top:2px"></i>'; }
            if (user.phone != null) { username += ' <i class="fa-solid fa-mobile-screen" title="' + "Número de telèfon verificat" + '" 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="' + "El compte està bloquejat" + '" style="margin-top:2px" />'; }
            if ((user.msghandle != null) && (features2 & 0x02000000)) { username += ' <i class="fa-solid fa-comment fa-xs" title="' + "Compte de missatgeria verificat" + '" 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) ? "Seleccioneu Cap" : "Seleccionar tot";
        }

        // 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>' + "Valida el correu electrònic" + '</option><option value=5>' + "Invalida el correu electrònic" + '</option>'; }

            var x = "Seleccioneu una operació per dur a terme en tots els usuaris seleccionats." + '<br /><br />';
            x += addHtmlFormFloating("Funcionament", '<select id=d3groupop class=form-select><option value=1>' + "Bloquejar el compte" + '</option><option value=2>' + "Desbloquejar el compte" + '</option>' + optionalActions + '<option value=3>' + "Esborrar compte" + '</option></select>');
            setModalContent('xxAddAgent', "Acció grupal", 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 = "Vols confirmar la supressió dels comptes seleccionats?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p3usersGroupActionFunctionDelCheck() />' + "Confirmeu" + '</label>';
                setModalContent('xxAddAgent', "Suprimeix els comptes", 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', "Envia 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 missatge", '<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="' + "Assignatura" + '"></input>';
            x += '<textarea id=d2emailText maxlength=10000 style="width:100%;height:200px;resize:none;overflow-y:auto" onKeyUp=showSendEmailValidate()></textarea>';

            setModalContent('xxAddAgent', "Envia un correu electrònic", 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>' + "Envia una notificació de text a aquest usuari." + '</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>' + "Mostra el missatge fins que l'usuari l'elimini" + '</option>';
            x += '<option value=10>' + "Mostra durant 10 segons" + '</option>';
            x += '<option value=60>' + "Mostra durant 1 minut" + '</option>';
            x += '<option value=300>' + "Mostra durant 5 minuts" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', format("Notificar a {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') + '>' + "Mostra només els 100 primers usuaris" + '</label><br />';
            setModalContent('xxAddAgent', "Vista d'usuaris", 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 = "Creeu molts comptes alhora important un fitxer JSON o CSV" + '<br /><br />';
            x += "El format de fitxer JSON és el següent:" + '<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 += "El format de fitxer CSV és el següent:" + '<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', "Importació del compte d'usuari", 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', "Importació del compte d'usuari", "Format de fitxer CSV no vàlid.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Importació del compte d'usuari", "Format de fitxer CSV no vàlid.");
                    }
                } else {
                    try {
                        j = JSON.parse(r.target.result);
                    } catch (ex) {
                        setModalContent('xxAddAgent', "Importació del compte d'usuari", format("Fitxer JSON no vàlid: {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', "Importació del compte d'usuari", "Format de fitxer JSON no vàlid.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Importació del compte d'usuari", "Format de fitxer JSON no vàlid.");
                    }
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            };
            fr.readAsText(Q('d4importFile').files[0]);
        }

        function p4downloadUserInfo() {
            if (xxdialogMode) return;
            var x = "Baixeu la llista d'usuaris amb un dels formats de fitxer següents." + '<br /><br />';
            x += addHtmlValue("Format CSV", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoCSV()\'>' + 'userlist.csv' + '</a>');
            x += addHtmlValue("Format JSON", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoJSON()\'>' + 'userlist.json' + '</a>');
            setModalContent('xxAddAgent', "Exportació de la llista d'usuaris", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

        function p4downloadUserInfoCSV() {
            var csv = "identificador,nom,correu electrònic,creació,darrer inici de sessió,grups,factors d'autenticació,administrador del lloc,administrador d'usuari,bloquejat" + '\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>' + "Emetre un missatge a tots els usuaris connectats." + '</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>' + "Mostra el missatge fins que l'usuari l'elimini" + '</option>';
            x += '<option value=10>' + "Mostra durant 10 segons" + '</option>';
            x += '<option value=60>' + "Mostra durant 1 minut" + '</option>';
            x += '<option value=300>' + "Mostra durant 5 minuts" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', "Missatge d'emissió", 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] == '') ? "Per defecte" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Domini", y);
            }

            if ((features & 0x200000) == 0) { x += addHtmlFormFloating('<span id=p4hname>' + "Nom d'usuari" + '</span>', '<input id=p4name class="form-control" maxlength=64 autocomplete=username onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />'); }
            x += addHtmlFormFloating('<span id=p4hemail>' + "Correu electrònic", '<input id=p4email type=email class="form-control" maxlength=256 autocomplete="email" inputmode="email" onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />');
            x += '';
            x += addHtmlFormFloating('<span id=p4hp1>' + "Contrasenya" + '</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" />' + "Aleatoriu la contrasenya." + '</label></div>';
            x += '<div><label><input id=p4removeEvents onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Elimina tots els esdeveniments anteriors per a aquest ID d'usuari." + '</label></div>';
            x += '<div><label><input id=p4resetNextLogin onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Força el restabliment de la contrasenya al proper inici de sessió." + '</label></div>';
            if (serverinfo.emailcheck) {
                x += '<div><label><input id=p4verifiedEmail onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "El correu electrònic està verificat." + '</label></div>';
                x += '<div><label><input id=p4invitationEmail type=checkbox class="form-check-input me-2" title=' + "Correu electrònic verificat i restabliment forçat de la contrasenya." + ' />' + "Envia un correu electrònic d'invitació." + '</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("Requisits: {0}.", r.join(', ')) + '</div>'; }
            }

            setModalContent('xxAddAgent', "Crear compte", 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 = "Introduïu una llista separada per comes de noms de dominis administratius." + '<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', "Àmbits administratius", 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>' + "Fitxers del servidor" + '</label>, <input type=number class="me-1 form-control" onchange=showUserAdminDialogValidate() maxlength=10 id=ua_fileaccessquota>' + "k màx, en blanc per defecte" + '</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>' + "Còpia de seguretat del servidor" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverrestore>' + "Restauració del servidor" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverupdate>' + "Actualitzacions del servidor" + '</label><br></div>';
            x += '<div id=d2MngUsr><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusers>' + "Gestionar Usuaris" + '</label><br></div>';
            x += '<div id=d2MngGrp><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusergroups>' + "Gestionar grups d'usuaris" + '</label><br></div>';
            x += '<div id=d2MngRec><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_managerecordings>' + "Gestionar les gravacions" + '</label><br></div>';
            x += '<div id=d2AllEvnt><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_allevents>' + "Veure tots els esdeveniments" + '</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>' + "Bloquejar el compte" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewgroups>' + "No hi ha grups de dispositius nous" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewdevices>' + "No hi ha dispositius nous" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nomeshcmd>' + "Sense eines (MeshCmd/encaminador)" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_locksettings>' + "Bloqueja la configuració del compte" + '</label><br>';
            x += '</div>';

            setModalContent('xxAddAgent', "Permisos del 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 s'han trobat grups." + '<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>' + "Nom" + '<th style=width:80px>' + "Usuaris" + '<th style=width:80px>' + "Grups de dispositius" + '<th style=width:80px>' + "Dispositius";
                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) ? "Seleccioneu Cap" : "Seleccionar tot";
        }

        // 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 = "Seleccioneu una operació per dur a terme en tots els usuaris seleccionats." + '<br /><br />';
            x += addHtmlFormFloating("Funcionament", '<select id=d50groupop class=form-select><option value=1>' + "Suprimeix el grup" + '</option></select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Acció grupal", 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 = "Vols confirmar la supressió dels grups d'usuaris seleccionats?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p50usersGroupActionFunctionDelCheck() />' + "Confirmeu" + '</label>';
                setModalContent('xxAddAgent', "Suprimeix els grups d'usuaris", 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("Grup d'usuaris", '<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] == '') ? "Per defecte" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Domini", y);
            }
            x += addHtmlFormFloating("Nom", '<input id=p4name class=form-control maxlength=64 onchange=showCreateUserGroupDialogValidate() onkeyup=showCreateUserGroupDialogValidate() />');
            x += addHtmlFormFloating("Descripció", '<textarea id=p4desc class=form-control value="" maxlength=1024 /></textarea>');
            setModalContent('xxAddAgent', (mode == 1) ? "Crear un grup d'usuaris" : "Grup d'usuaris duplicat", 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>' + "Cap" + '</i>'; }
            if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { gname = '<span role=button tabindex=0 title="' + "Feu clic aquí per editar el nom del grup d'usuaris" + '" 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>' + "Cap" + '<i>'; } else { desc = EscapeHtml(desc); }

            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Nom", 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("Domini", (d != '') ? EscapeHtml(d) : ('<i>' + "Per defecte" + '</i>'));
                x += addDeviceAttribute("Identificador de grup", EscapeHtml(group._id));
            }
            if (currentUserGroup.membershipType != null) {
                x += addDeviceAttribute("Tipus de grup", EscapeHtml(currentUserGroup.membershipType));
            }
            if ((userinfo.siteadmin & 256) != 0) {
                x += addDeviceAttribute("Descripció", '<span role=button onclick=p51editgroup(2,' + (currentUserGroup.membershipType != null) + ')>' + desc + ' <i class="fa-solid fa-pencil fa-xs"/></i></span>');
            } else {
                x += addDeviceAttribute("Descripció", desc);
            }

            // Display features
            if (serverinfo.userGroupsSessionRecording == 1) {
                var userGroupFeatures = [];
                if ((group.flags) && (group.flags & 2)) { userGroupFeatures.push("Sessions de gravació"); }
                userGroupFeatures = userGroupFeatures.join(', ');
                if (userGroupFeatures == '') { userGroupFeatures = '<i>' + "Cap" + '</i>'; }
                x += addDeviceAttribute("Característiques", 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("Petició de l'escriptori+barra d'eines"); } else if (consent & 0x0040) { consentOptionsStr.push("Barra d'eines d'escriptori"); } else if (consent & 0x0008) { consentOptionsStr.push("Petició d'escriptori"); } else { if (consent & 0x0001) { consentOptionsStr.push("Notificació d'escriptori"); } }
                if (consent & 0x0010) { consentOptionsStr.push("Indicador del terminal"); } else { if (consent & 0x0002) { consentOptionsStr.push("Notificació de terminal"); } }
                if (consent & 0x0020) { consentOptionsStr.push("Solicitud de fitxers"); } else { if (consent & 0x0004) { consentOptionsStr.push("Notificació de fitxers"); } }
                if (consent == 7) { consentOptionsStr = ["Notificar sempre"]; }
                if ((consent & 56) == 56) { consentOptionsStr = ["Sempre prompte"]; }

                consentOptionsStr = consentOptionsStr.join(', ');
                if (consentOptionsStr == '') { consentOptionsStr = '<i>' + "Cap" + '</i>'; }
                x += addDeviceAttribute("Consentiment de l'usuari", addLinkConditional(consentOptionsStr, 'p20editmeshconsent(4)', true));
            }

            x += addDeviceAttribute("Usuaris", usercount);
            x += addDeviceAttribute("Grups de dispositius", meshcount);
            x += addDeviceAttribute("Dispositius", devicecount);

            x += '</table></div><br />';

            if ((userinfo.siteadmin & 256) != 0) {
                x += '<input type=button class="btn btn-primary btn-sm" value="' + "Transmissió" + '" title="' + "Envieu un avís a tots els usuaris d'aquest grup." + '" 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> ' + "Afegeix usuaris" + '</button>';
            }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Membres del grup" + '</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="' + "Elimina els drets d'usuari d'aquest grup de dispositius" + '" 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="' + "Usuari" + '"></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>' + "No hi ha membres" + '</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> ' + "Afegeix un grup de dispositius" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Grups de dispositius comuns" + '</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>' + "Grup de dispositius desconegut" + '</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="' + "Elimina els drets del grup d'usuaris a aquest grup de dispositius" + '" 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="' + "Grup de dispositius" + '" 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 hi ha cap grup de dispositius en comú" + '</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> ' + "Afegeix un dispositiu" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Dispositius comuns" + '</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>' + "Dispositiu desconegut" + '</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="' + "Elimina els drets del grup d'usuaris d'aquest dispositiu" + '" 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="' + "Grup de dispositius" + '" 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>' + "No hi ha dispositius en comú" + '</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>' + "Suprimeix el grup d'usuaris" + '</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' : '') + '>' + "Gravar sessions" + '</label><br></div>';
            }
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Edita les funcions del grup d'usuaris", 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', "Elimina els permisos del dispositiu", format("Confirmeu l'eliminació dels drets d'accés per al dispositiu \"{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', "Elimina els permisos del grup de dispositius", format("Confirmeu l'eliminació dels drets d'accés per al grup de dispositius \"{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("Nom", '<input id=dp51name class="form-control" maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) ' + (nameReadOnly ? 'readonly disabled' : '') + '/>');
            x += addHtmlFormFloating("Descripció", '<textarea id=dp51desc class="form-control" maxlength=1024 style=width:100%;resize:none></textarea>');

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Edita el grup d'usuaris", 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("Vols suprimir el grup d'usuaris {0}?", EscapeHtml(currentUserGroup.name)) + '<br /><br />';
            x += '<label><input id=p51check type=checkbox class="form-check-input me-2" onchange=p51validateDeleteGroupDialog() />' + "Confirmeu" + '</label>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Suprimeix el grup d'usuaris", 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', "Elimina la pertinença d'usuari", format("Vols confirmar l'eliminació de la pertinença a l'usuari \"{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 = "Permet als usuaris gestionar aquest grup de dispositius i els dispositius d'aquest grup.";
            if (features & 0x00080000) { x += " Els usuaris han d'iniciar sessió a aquest servidor una vegada abans de poder afegir-se a un grup de dispositius." }
            x += '<br /><br /><div style=\'position:relative\'>';
            x += addHtmlFormFloating("Identificadors d'usuari", '<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', "Afegeix usuaris al grup d'usuaris", 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="' + "El compte està bloquejat" + '" style="margin-top:2px" /> '; msg.push("Compte bloquejat"); }
            if ((user.siteadmin == null) || ((user.siteadmin & (0xFFFFFFFF - 1248)) == 0)) { msg.push("Sense drets de servidor"); } else if (user.siteadmin == 8) { msg.push("Accés als fitxers del servidor"); } else if (user.siteadmin == 0xFFFFFFFF) { msg.push("Administrador total"); } else { msg.push("Drets parcials"); }
            if ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & (64 + 128 + 1024)) != 0)) { msg.push("Restriccions"); }

            // Show user attributes
            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Nom", user.name); } // If title bar is hidden, display the user name here
            var email = user.email ? EscapeHtml(user.email) : '<i>' + "No configurat" + '</i>', everify = '';
            var realname = user.realname ? EscapeHtml(user.realname) : '<i>' + "No configurat" + '</i>';
            if (serverinfo.emailcheck) { everify = ((user.emailVerified == true) ? '<b style=color:green;cursor:pointer title="' + "El correu electrònic està verificat" + '">&#x2713</b> ' : '<b style=color:red;cursor:pointer title="' + "Correu electrònic no verificat" + '">&#x2717;</b> '); }

            if ((serverinfo.crossDomain) || (debugmode != 0)) {
                var d = user._id.split('/')[1];
                x += addDeviceAttribute("Domini", ((d != '') ? EscapeHtml(d) : ('<i>' + "Per defecte" + '</i>')));
                x += addDeviceAttribute("Identificador d'usuari", EscapeHtml(user._id));
            } else {
                if (user.name.toLowerCase() != user._id.split('/')[2]) { x += addDeviceAttribute("Identificador d'usuari", 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("Correu electrònic", '<span role=button onclick=p30showUserEmailChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + everify + email + emailLink + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
                x += addDeviceAttribute("Nom real", '<span role=button onclick=p30showUserRealNameChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + realname + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            } else {
                x += addDeviceAttribute("Correu electrònic", everify + email + emailLink);
                x += addDeviceAttribute("Nom 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èfon", '<span role=button onclick=p30editPhone()>' + (user.phone ? user.phone : ('<i>' + "Cap" + '</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("Missatgeria", '<span role=button onclick=p30editMessaging()><i class="fa-solid fa-comment fa-xs" title="' + "Missatgeria activada" + '" style="margin-top:2px"></i> ' + (user.msghandle ? user.msghandle : ('<i>' + "Cap" + '</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("Sessions de gravació"); }
            if (user.removeRights) {
                if ((user.removeRights & 0x00000008) != 0) { userFeatures.push("Sense comandament a distància"); } else {
                    if ((user.removeRights & 0x00010000) != 0) { userFeatures.push("Sense escriptori"); }
                    else if ((user.removeRights & 0x00000100) != 0) { userFeatures.push("Només visualització d'escriptori"); }
                    if ((user.removeRights & 0x00000200) != 0) { userFeatures.push("Sense terminal"); }
                    if ((user.removeRights & 0x00000400) != 0) { userFeatures.push("No hi ha fitxers"); }
                }
                if ((user.removeRights & 0x00000010) != 0) { userFeatures.push("Sense consola"); }
                if ((user.removeRights & 0x00008000) != 0) { userFeatures.push("Sense desinstal·lació"); }
                if ((user.removeRights & 0x00020000) != 0) { userFeatures.push("Sense comandament remot"); }
                if ((user.removeRights & 0x00000040) != 0) { userFeatures.push("Sense despertar"); }
                if ((user.removeRights & 0x00040000) != 0) { userFeatures.push("Sense restabliment/apagat"); }
            }
            userFeatures = userFeatures.join(', ');
            if (userFeatures == '') { userFeatures = '<i>' + "Cap" + '</i>'; }
            x += addDeviceAttribute("Característiques", addLink(userFeatures, 'p20edituserfeatures()'));

            x += addDeviceAttribute("Drets 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("Quota de servidor", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
            x += addDeviceAttribute("Creació", printDateTime(new Date(user.creation * 1000)));
            if (user.login) x += addDeviceAttribute("Últim inici de sessió", printDateTime(new Date(user.login * 1000)));
            if (user.passchange == -1) { x += addDeviceAttribute("Contrasenya", "Es canviarà el proper inici de sessió."); }
            else if (user.passchange) { x += addDeviceAttribute("Contrasenya", format("Últim canvi: {0}", printDateTime(new Date(user.passchange * 1000)))); }

            // Device Groups
            var linkCount = 0, linkCountStr = '<i>' + "Cap" + '<i>';
            if (user.links) {
                for (var i in user.links) { if (i.startsWith('mesh/')) { linkCount++; } }
                if (linkCount == 1) { linkCountStr = "1 grup"; } else if (linkCount > 1) { linkCountStr = format("{0} grups", linkCount); }
            }
            x += addDeviceAttribute("Grups de dispositius", linkCountStr);

            // Administrative Realms
            if ((userinfo.siteadmin == 0xFFFFFFFF) || (userinfo.siteadmin & 2)) {
                var xuserGroups = '<i>' + "Cap" + '</i>';
                if (user.groups) { xuserGroups = ''; for (var i in user.groups) { xuserGroups += '<span class="tagSpan">' + EscapeHtml(user.groups[i]) + '</span>'; } }
                x += addDeviceAttribute("Àmbits d'administració", 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("Petició de l'escriptori+barra d'eines"); } else if (consent & 0x0040) { meshFeatures.push("Barra d'eines d'escriptori"); } else if (consent & 0x0008) { meshFeatures.push("Petició d'escriptori"); } else { if (consent & 0x0001) { meshFeatures.push("Notificació d'escriptori"); } }
                if (consent & 0x0010) { meshFeatures.push("Indicador del terminal"); } else { if (consent & 0x0002) { meshFeatures.push("Notificació de terminal"); } }
                if (consent & 0x0020) { meshFeatures.push("Solicitud de fitxers"); } else { if (consent & 0x0004) { meshFeatures.push("Notificació de fitxers"); } }
                if (consent == 7) { meshFeatures = ["Notificar sempre"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Sempre prompte"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Cap" + '</i>'; }
                x += addDeviceAttribute("Consentiment de l'usuari", 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ó d'autenticació"); }
                if (user.otphkeys > 0) { factors.push("Clau de seguretat"); }
                if (user.otpekey > 0) { factors.push("Correu electrònic"); }
                if (user.otpduo > 0) { factors.push("Duo"); }
                if (user.otpkeys > 0) { factors.push("Codis de còpia de seguretat"); }
                if (user.otpdev > 0) { factors.push("Push del dispositiu"); }
                if ((user.phone != null) && (features & 0x04000000)) { factors.push("SMS"); }
                if ((user.msghandle != null) && (features2 & 0x04000000)) { factors.push("Missatgeria"); }
                x += addDeviceAttribute("Seguretat", '<i class="fa-solid fa-key" title="' + "S'ha activat l'autenticació del segon factor" + '" 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="' + "Notes" + '" title="' + "Veure notes sobre aquest usuari" + '" 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="' + "Envia un missatge SMS a aquest usuari" + '" onclick=showSendSMS("' + encodeURIComponentEx(user._id) + '") />'; }
            if (user.msghandle && (features2 & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Missatge" + '" title="' + "Envia un missatge a aquest usuari" + '" 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="' + "Correu electrònic" + '" title="' + "Envieu un missatge de correu electrònic a aquest usuari" + '" 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="' + "Envia una notificació a l'usuari" + '" onclick=showUserAlertDialog(event,"' + encodeURIComponentEx(user._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Xatejar" + '" title="' + "Xatejar" + '" 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="' + "Elimina aquest usuari" + '">' + "Suprimeix l'usuari" + '</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="' + "Canvia la contrasenya d'aquest usuari" + '">' + "Canvia la contrasenya" + '</a>';
                x += ' <a href=# style=cursor:pointer onclick=\'return p30viewPreviousLogins()\' title="' + "Veure els inicis de sessió anteriors d'aquest usuari" + '">' + "Inicis de sessió anteriors" + '</a>';
            }
            x += '</div><br>'
            QH('p30html3', x);

            // Update user's connection state
            x = '';
            if (activeSessions == 1) { x = "1 sessió activa"; } else if (activeSessions > 1) { x = format("{0} sessions actives", 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èfon compatible amb SMS per a aquest usuari." + '<br />' + "Deixeu-ne en blanc per a cap.";
            x += '<br /><br /><div style=width:100%;text-align:center>' + "Número de telèfon:" + ' <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', "Notificacions telefòniques", 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%>' + "Compte de missatgeria per a aquest usuari.";
            var y = '<select id=d2serviceselect style=width:160px;margin-left:8px onchange=p30editMessagingValidate()><option value=0>' + "Cap" + '</option>';
            if ((serverinfo.userMsgProviders & 1) != 0) { y += '<option value=1>' + "Telegrama" + '</option>'; }
            if ((serverinfo.userMsgProviders & 4) != 0) { y += '<option value=4>' + "Discòrdia" + '</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>' + "Empènyer" + '</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>' + "Fluix" + '</option>'; }
            y += '</select>';
            x += '<table style=margin-top:12px><tr><td>' + "Servei" + '<td>' + y;
            x += '<tr><td>' + "Gestionar" + '<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">' + "Uneix-te a aquest servidor de Discord per rebre notificacions." + '</a></div>' }
            x += '<div id=d2callmebotinfo style=display:none><br /><a href=https://www.callmebot.com/blog/free-api-signal-send-messages/ target="_callmebot">' + "Senyal" + '</a>, <a href=https://www.callmebot.com/blog/free-api-whatsapp-messages/ target="_callmebot">' + "què tal" + '</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">' + "Telegrama" + '</a></div>';
            x += '<div id=d2pushoverinfo style=display:none><br /><a href=https://pushover.net/ target="_pushover">' + "Informació a Pushover.net" + '</a></div>';
            x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Servei gratuït a ntfy.sh" + '</a></div>';
            x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Configuració de Slack Webhook" + '</a></div>';
            xxdialogButtons = 3; xxdialogTag = 'verifyMessaging';
            setModalContent('xxAddAgent', "Notificacions de missatgeria", 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'] = "Nom d'usuari: 0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "nom d'usuari@server.com"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Clau d'usuari"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Tema"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "nom d'usuari@sample.com"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Nom d'usuari"; }
            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' : '') + '>' + "Gravar sessions" + '</label><br></div>';
            }
            x += '<div><label><input type=checkbox id=d20flag7 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000008) ? 'checked' : '') + '>' + "Sense comandament a distància" + '</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' : '') + '>' + "Sense accés a l'escriptori" + '</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' : '') + '>' + "Només visualització 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' : '') + '>' + "Sense accés al 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' : '') + '>' + "Sense accés a fitxers" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag6 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000010) ? 'checked' : '') + '>' + "Sense consola d'agent" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag8 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00008000) ? 'checked' : '') + '>' + "Sense desinstal·lació" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag9 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00020000) ? 'checked' : '') + '>' + "Sense comandament remot" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag10 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000040) ? 'checked' : '') + '>' + "Sense despertar" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag11 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00040000) ? 'checked' : '') + '>' + "Sense restabliment/apagat" + '</label><br></div>';

            setModalContent('xxAddAgent', "Edita les funcions d'usuari", 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("Canvia el nom 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("Estat", '<select id=dp30verified class="form-control" onchange=p30validateEmail()><option value=0>' + "No verificat" + '</option><option value=1>' + "Verificat" + '</option></select>'); }

            setModalContent('xxAddAgent', format("Canvia el correu electrònic per a {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', "Inicis de sessió anteriors", '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("Requisits: {0}.", r.join(', ')) + '</div>'; }
            }

            x += '<div><label><input id=p4resetNextLogin class="form-check-input me-2" type=checkbox />' + "Força el restabliment de la contrasenya al proper inici de sessió." + '</label></div>';
            if (multiFactor == 1) { x += '<div><label><input id=p4twoFactorRemove class="form-check-input me-2" type=checkbox />' + "Elimina tota l'autenticació del segon factor." + '</label></div>'; }
            setModalContent('xxAddAgent', format("Canvia la contrasenya per a {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("Suprimeix l'usuari {0}", EscapeHtml(currentUser.name)), format("Confirmeu la supressió de l'usuari {0}?", EscapeHtml(currentUser.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Confirmeu" + '</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> ' + "Afegeix un grup de dispositius" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Grups de dispositius comuns" + '</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>' + "Grup de dispositius desconegut" + '</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="' + "Elimina els drets d'usuari d'aquest grup de dispositius" + '" 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="' + "Grup de dispositius" + '" 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 hi ha cap grup de dispositius en comú" + '</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> ' + "Afegeix un grup d'usuaris" + '</button>'; }
                }
                x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Pertinences a grups d'usuaris" + '</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>' + "Grup d'usuaris desconegut" + '</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="' + "Elimina la pertinença al grup d'usuaris" + '" 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="' + "Grup d'usuaris" + '" 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 hi ha membres de grups d'usuaris" + '</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> ' + "Afegeix un dispositiu" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Dispositius comuns" + '</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>' + "Dispositiu desconegut" + '</i>');
                    if ((cr & 2) != 0) {
                        trash = '<a href=# onclick=\'return p30removeNodeFromUser(event,"' + encodeURIComponentEx(node._id) + '")\' title="' + "Elimina els drets d'usuari d'aquest grup de dispositius" + '" 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="' + "Dispositiu" + '" 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>' + "No hi ha dispositius en comú" + '</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', "Elimina la compartició del dispositiu", format("Voleu confirmar l'eliminació del dispositiu compartit \"{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', "Elimina els permisos del dispositiu", format("Confirmeu l'eliminació dels drets d'accés per al dispositiu \"{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', "Elimina els permisos d'usuari", name ? format("Confirmeu l'eliminació dels drets d'accés per a l'usuari \"{0}\"?", name) : "Confirmeu l'eliminació dels drets d'accés?");
                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', "Elimina els permisos del grup d'usuaris", name ? format("Confirmeu l'eliminació dels drets d'accés per al grup d'usuaris \"{0}\"?", name) : "Confirmeu l'eliminació dels drets d'accés?");
                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>' + "Desconegut" + '</i>');
            setModalContent('xxAddAgent', "Elimina la pertinença a un grup d'usuaris", format("Vols confirmar l'eliminació de la pertinença al grup d'usuaris \"{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("Grup d'usuaris", '<select id=dp2groupid class="form-select">' + y + '</select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Afegeix membres", 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', "Elimina els permisos del grup de dispositius", format("Confirmeu l'eliminació dels drets d'accés per al grup de dispositius \"{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 ? "Toggle Light Mode" : "Toggle Dark Mode";
                    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="' + "Aquesta és una sessió d'intercanvi de convidats" + '">' + 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="' + "Aquesta és una sessió d'intercanvi de convidats" + '">' + 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 s'han trobat esdeveniments" + '</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>' + "Carregant..." + '</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 pot llegir des de la carpeta d'enregistraments." + '</i></div>'; }
                else if (p52recordings == 2) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "El servidor no pot obtenir enregistraments de la base de dades." + '</i></div>'; }
                else { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "S'ha produït un error desconegut." + '</i></div>'; }
            } else if (p52recordings.length == 0) {
                x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "No hi ha enregistraments." + '</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>' + "Sessió" + '</th><th style=width:110px>' + nobreak("L'hora d'inici") + '</th><th style=width:110px>' + "Durada" + '</th><th style=width:110px>' + "Mida" + '</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>' + "Desconegut" + '</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 += ' - ' + "Sessió terminal"; }
            if (rec.protocol == 2) { sessionName += ' - ' + "Sessió d'escriptori"; }
            if (rec.protocol == 5) { sessionName += ' - ' + "Transferència d'arxius"; }
            if (rec.protocol == 6) { sessionName += ' - ' + "Administrador PowerShell"; }
            if (rec.protocol == 8) { sessionName += ' - ' + "Shell d'usuari"; }
            if (rec.protocol == 9) { sessionName += ' - ' + "Usuari PowerShell"; }
            if (rec.protocol == 100) { sessionName += ' - ' + "Intel&reg; AMT WSMAN"; }
            if (rec.protocol == 101) { sessionName += ' - ' + "Redirecció Intel&reg; AMT"; }
            if (rec.protocol == 200) { sessionName += ' - ' + "missatger"; }

            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 = "Desconegut";
                if (rec.protocol == 1) { protocolStr = "Terminal"; }
                if (rec.protocol == 2) { protocolStr = "Escriptori"; }
                if (rec.protocol == 5) { protocolStr = "Fitxers"; }
                if (rec.protocol == 6) { protocolStr = "Administrador PowerShell"; }
                if (rec.protocol == 8) { protocolStr = "Shell d'usuari"; }
                if (rec.protocol == 9) { protocolStr = "Usuari PowerShell"; }
                if (rec.protocol == 100) { protocolStr = "Intel&reg; AMT WSMAN"; }
                if (rec.protocol == 101) { protocolStr = "Redirecció Intel&reg; AMT"; }
                if (rec.protocol == 200) { protocolStr = "missatger"; }
                x += addHtmlValue4("Protocol", protocolStr);
            }
            x += addHtmlValue4("Estat", (rec.present == 1) ? "Present al servidor" : "No al servidor");
            if (rec.name) { x += addHtmlValue4("Nom del dispositiu", EscapeHtml(rec.name)); }
            if (rec.meshname) { x += addHtmlValue4("Grup de dispositius", EscapeHtml(rec.meshname)); }
            if (rec.size) { x += addHtmlValue4("Mida", format("{0} bytes", rec.size)); }
            if (rec.startTime) { x += addHtmlValue4("L'hora d'inici", printTime(new Date(rec.startTime))); }
            if (rec.startTime && rec.lengthTime) { x += addHtmlValue4("Fi del temps", printTime(new Date(rec.startTime + (rec.lengthTime * 1000)))); }
            if (rec.lengthTime) { x += addHtmlValue4("Durada", 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", "Habilitat"); }
            if (rec.userids) { for (var i in rec.userids) { x += addHtmlValue4("Usuari", rec.userids[i].split('/')[2]); } }

            xxdialogButtons = 9;
            setModalContent('xxAddAgent', "Detalls de gravació", 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: "Sessions a distància", 2: "Ús del trànsit d'usuaris", 3: "Inicis de sessió d'usuari" }
            if (userinfo.siteadmin == 0xFFFFFFFF) { options[4] = "Registres de bases de dades"; }
            for (var i in options) { y += '<option value=' + i + ((settings.type == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Tipus", '<select id=d2reportType class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');

            x += '<div id=d2GroupByDiv style=display:none>';
            y = '';
            var options = { 1: "Usuari", 2: "Dispositiu", 3: "Dia" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Agrupar per", '<select id=d2groupBy class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2GroupByDiv2 style=display:none>';
            y = '';
            var options = { 1: "Usuari", 3: "Dia" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy2 == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Agrupar per", '<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' : '') + '>' + "Tots" + '</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("Grup de dispositius", '<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: "L'últim dia", 7: "Últims 7 dies", 30: "Últims 30 dies", 0: "Interval de temps" }
            for (var i in options) { y += '<option value=' + i + ((settings.timeRange == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Temps", '<select id=d2timeRange class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2timeRangeDiv style=display:none>';
            x += addHtmlFormFloating("Interval de temps d'inici", '<input id=d2timeRangeStartSelector class="flatpickr form-control" type="text" placeholder="' + "Seleccioneu la data i l'hora..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Final de l'interval de temps", '<input id=d2timeRangeEndSelector class="flatpickr form-control" type="text" placeholder="' + "Seleccioneu la data i l'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' : '') + '>' + "Mostra el trànsit" + '</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: "Temps", device: "Dispositiu", session: "Sessió", user: "Usuari", devgroup: "Grup de dispositius", guest: "Convidat", length: "Llargada", bytesin: "Bytes In", bytesout: "Bytes fora", os: "Sistema operatiu", ip: "Adreça IP", browser: "Navegador", msg: "Missatge", twofactor: "2n 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>' + "L'informe no ha retornat cap complet." + '</tr></td>';
            } else {
                reportCSV = '"' + "Grup" + '"';
                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 "Escriptori";
                if (v == 5) return "Fitxers";
                if (v == 6) return "Administrador PowerShell";
                if (v == 8) return "Shell d'usuari";
                if (v == 9) return "Usuari PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "missatger";
                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 "Desconegut" + ' (' + 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>' + "Desconegut" + '</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>' + "Desconegut" + '</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>' + "Desconegut" + '</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>' + "Desconegut" + '</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>' + "Usuari desconegut" + '</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>' + "Inici de sessió satisfactori";
                if (v == 108) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny2"></div>' + "2n factor incorrecte";
                if (v == 109) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny4"></div>' + "Compte bloquejat";
                if (v == 110) return '<div style=float:left;margin-right:4px;cursor:pointer><i class="fa-solid fa-exclamation-triangle text-warning"></i></div>' + "Intent d'inici de sessió no vàlid";
            }
            if (f == '2fa') {
                if (v == '') { return "Cap"; }
                if (v == 'backup') { return "Codis de còpia de seguretat"; }
                if (v == 'fido') { return "Tecla FIDO"; }
                if (v == 'sms') { return "Missatge SMS"; }
                if (v == 'hwotp') { return "Maquinari OTP"; }
                if (v == 'messenger') { return "Missatgeria"; }
                if (v == 'push') { return "Activa notificacions"; }
                if (v == 'otp') { return "Contrasenya única"; }
                if (v == 'cookie') { return "Recorda el dispositiu"; }
                if (v == 'tokenlogin') { return "Token d'inici de sessió"; }
                if (v == 'ipaddr') { return "Adreça IP"; }
                if (v == 'sso') { return "Inici de sessió únic"; }
            }
            if (f == 'records') {
                var c = {
                    'node': "Registre del dispositiu",
                    'mesh': "Registre del grup de dispositius",
                    'user': "Registres d'usuari",
                    'sysinfo': "Registres d'informació del dispositiu",
                    'iploc': "Registres d'informació d'ubicació IP",
                    'note': "Registres de notes de text",
                    'ifinfo': "Registres d'informació de la interfície de xarxa",
                    'cfile': "Registres del fitxer de configuració",
                    'lastconnect': "Registres de l'última connexió",
                    'power': "Registres de transició de potència del dispositiu",
                    'events': "Registres d'esdeveniments",
                    'serverstats': "Registres d'estadístiques del servidor",
                    'ugrp': "Registres del grup d'usuaris",
                    'deviceshare': "Registres per compartir dispositius",
                    'logintoken': "Registres de testimoni d'inici de sessió del compte",
                    'smbios': "Registre SMBIOS del dispositiu",
                    'pmt': "Registre de notificació push del dispositiu",
                    'meshcentral': "Dispositiu, usuaris, grups i altres registres"
                }
                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 "Escriptori";
                if (v == 5) return "Fitxers";
                if (v == 6) return "Administrador PowerShell";
                if (v == 8) return "Shell d'usuari";
                if (v == 9) return "Usuari PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "missatger";
                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 "Desconegut" + ' (' + 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 "Usuari desconegut";
                }
            }
            if (f == 'msg') {
                if (v == 107) return "Inici de sessió satisfactori";
                if (v == 108) return "2n factor incorrecte";
                if (v == 109) return "Compte bloquejat";
                if (v == 110) return "Intent d'inici de sessió no vàlid";
            }
            if (f == 'records') { return renderReportFormat(v, f); }
            return '' + v;
        }

        function p60downloadReport() {
            if (xxdialogMode || (reportCSV == null)) return;
            saveAs(stringToUtf8Blob(reportCSV), "Informe.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>' + "Actualment no hi ha notificacions" + '</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("Va tenir lloc a les {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="' + "Esborra aquesta notificació" + '" 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">' + "Esborra-ho tot" + '</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,
                "Compte nou", // 1
                "Límit del servidor",
                "Avís de seguretat",
                "Configuració del compte",
                "Grup de dispositius",
                "Codis d'invitació"
            ];
            var translatedMessages = [
                null,
                "Permís denegat", // 1
                "Nom d'usuari no vàlid",
                "Contrasenya invàlida",
                "Email invàlid",
                "Domini no vàlid",
                "Permisos de lloc no vàlids",
                "L'usuari ja existeix",
                "No es pot afegir usuari en aquest mode",
                "Excepció de validació",
                "S'ha arribat al límit del compte.", // 10
                "Sol·licitud de xat, feu clic aquí per acceptar.",
                "Hi ha hagut {0} intents fallits d'inici de sessió en aquest compte des de l'última sessió.",
                "No s'ha pogut canviar l'adreça electrònica, un altre compte ja utilitza: {0}.",
                "Email enviat.",
                "No s'ha trobat l'usuari {0}.",
                "No s'han trobat usuaris {0}.",
                "Error, no es pot canviar a la contrasenya utilitzada anteriorment.",
                "Error, no es pot canviar a la contrasenya d'ús habitual.",
                "Error, la contrasenya no s'ha canviat.",
                "La contrasenya ha canviat.", // 20
                "La contrasenya actual no és correcta.",
                "Error, el codi d'invitació \"{0}\" ja s'utilitza.",
                "La passarel·la d'SMS no està activada",
                "Sense drets de gestió d'usuaris",
                "Missatge SMS no vàlid",
                "No hi ha cap número de telèfon per a aquest usuari",
                "SMS enviat correctament.",
                "Error d'SMS",
                "Error d'SMS: {0}",
                "El domini de correu electrònic \"{0}\" no està permès. Només es permet ({1}).", // 30,
                "Missatge no vàlid",
                "Missatge enviat correctament.",
                "Error de missatge",
                "Error de missatge: {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: ["Usat", "Gratuït"] },
                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: ["Usat", "Gratuït"] },
                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: "Estat del servidor",
                AgentErrorCounters: "Comptadors d'errors de l'agent",
                UnknownGroup: "Grup desconegut",
                InvalidPKCSsignature: "Signatura PKCS no vàlida",
                InvalidRSAsignature: "Signatura RSA no vàlida",
                InvalidJSON: "JSON no vàlid",
                UnknownAction: "Acció desconeguda",
                BadWebCertificate: "Certificat web incorrecte",
                BadSignature: "Mala signatura",
                MaxSessionsReached: "Màxim de sessions assolits",
                UnknownDeviceGroup: "Grup de dispositius desconegut",
                InvalidDeviceGroupType: "Tipus de grup de dispositius no vàlid",
                DuplicateAgent: "Agent duplicat",
                ConnectedIntelAMT: "Intel&reg; AMT connectat",
                ConnectedIntelAMTCira: "Intel&reg; AMT CIRA connectat",
                RelayErrors: "Errors de relé",
                UserAccounts: "Comptes d'usuari",
                DeviceGroups: "Grups de dispositius",
                AgentSessions: "Sessions d'agents",
                ConnectedUsers: "Usuaris connectats",
                UsersSessions: "Sessions d'usuaris",
                RelaySessions: "Sessions de relleu",
                RelayCount: "Recompte de relleus"
            };

            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>' + "Càrrega de la CPU" + '</div><div><b title="' + "Càrrega de la CPU en l'últim minut" + '">' + (Math.round(message.cpuavg[0] * 100.0) / 100.0) + '</b>, <b title="' + "Càrrega de la CPU en els últims 5 minuts" + '">' + (Math.round(message.cpuavg[1] * 100.0) / 100.0) + '</b>, <b title="' + "Càrrega de la CPU en els darrers 15 minuts" + '">' + (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>' + "Memòria disponible" + '</div><div><b>' + getNiceSize3(message.availablemem) + '</b> ' + "lliure" + ', <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>' + "Memòria" + '</div><div><b>' + getNiceSize3(message.freemem) + '</b> ' + "lliure" + ', <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 = ["Agent", "CIRA", "AMT-OS", "HTTP", "Relleu", "Terminal", "Escriptori", "Fitxers", "WebRDP", "WebSSH", "WebVNC", "Multiplex d'escriptori"];
        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 = "Recompte de connexions";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Agents", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Usuaris", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Sessions d'usuari", data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: "Sessions de relleu", 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: "Extern", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Munt utilitzat", 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 = "Ús";
                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' : '') + '>' + "Nul" + '</option>'; }
                QH('p40server', x);
            }
            QV('p40server', servers.length > 0);
        }

        function p40downloadEvents() {
            var csv = "temps, 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', "Esdeveniment de rastreig 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 principal" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c1 ' + ((serverTraceSources.indexOf('cookie') >= 0) ? 'checked' : '') + '>' + "Codificador de galetes" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c2 ' + ((serverTraceSources.indexOf('dispatch') >= 0) ? 'checked' : '') + '>' + "Distribuïdor de missatges" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c3 ' + ((serverTraceSources.indexOf('main') >= 0) ? 'checked' : '') + '>' + "Missatges del servidor principal" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c4 ' + ((serverTraceSources.indexOf('peer') >= 0) ? 'checked' : '') + '>' + "Emparejat del servidor MeshCentral" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c15 ' + ((serverTraceSources.indexOf('agent') >= 0) ? 'checked' : '') + '>' + "Trànsit MeshAgent" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c14 ' + ((serverTraceSources.indexOf('agentupdate') >= 0) ? 'checked' : '') + '>' + "Actualització de MeshAgent" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c16 ' + ((serverTraceSources.indexOf('cert') >= 0) ? 'checked' : '') + '>' + "Certificat de servidor" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c17 ' + ((serverTraceSources.indexOf('db') >= 0) ? 'checked' : '') + '>' + "Base de dades del servidor" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c18 ' + ((serverTraceSources.indexOf('email') >= 0) ? 'checked' : '') + '>' + "Correu electrònic/SMS/Push Trànsit" + '</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' : '') + '>' + "Sol·licituds del 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' : '') + '>' + "Capçaleres 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' : '') + '>' + "Registre d'autenticació d'usuari" + '</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' : '') + '>' + "Gestor Intel AMT" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c9 ' + ((serverTraceSources.indexOf('webrelay') >= 0) ? 'checked' : '') + '>' + "Relé de connexió" + '</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' : '') + '>' + "Ordres 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', "Seguiment 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 = "temps, font, missatge" + '\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, "Seguretat del compte", 1, null, "No es pot accedir a aquesta funció fins que s'hagi activat l'autenticació de dos factors. Això és necessari per a una seguretat addicional. Aneu a \"El meu compte\" i mireu la secció \"Seguretat del compte\"."); 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', "Seguretat del compte", '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="' + "La versió no és compatible, primer actualitzeu la instal·lació 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="' + "La versió no és compatible, primer actualitzeu la instal·lació 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 = "S'està comprovant...";
                    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="' + "Veure registre de canvis" + '" rel="noreferrer noopener" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
                        } else {
                            cant_action.push('upgrade');
                            if (p.status) p.upgradeAvail = "Actualitzat";
                            else p.upgradeAvail = '<a title="' + "Veure registre de canvis" + '" 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ó del connector", format("Esteu segur que voleu {0} el connector: {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 connector" + ' - ' + 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> ' + "S'estan carregant els 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()">' + "Cancel · lar" + '</button>';
            x += '<button type="button" class="btn btn-primary" onclick="savePluginPermissionsEx()">' + "Desa" + '</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">' + "Aquest connector no ha definit cap permís." + '</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\')">' + "Malles" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary" id="btnTabNodes" onclick="switchPermTab(\'nodes\')">' + "Nodes" + '</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' : '') + '>✓ ' + "Permès" + '</option>';
                x += '<option value="denied"' + (defaultVal === 'denied' ? ' selected' : '') + '>✗ ' + "Negat" + '</option>';
                x += '<option value="inherited"' + (defaultVal === 'inherited' ? ' selected' : '') + '>↩ ' + "Heretat" + '</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">' + "Permès" + '</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">' + "Negat" + '</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="' + "Cerca usuaris, grups, dispositius" + '..." ';
            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' ? "Seleccioneu el grup de dispositius" : "Seleccioneu el dispositiu" ) + ' --</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ífics per a aquest grup de dispositius" : "Permisos específics per a aquest dispositiu") + '</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 de trucada", 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>