<!DOCTYPE html><html lang="uk" 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>Інформація</b></div>
        <div id="cxdesktop" class="cmtext" onclick="cmaction(3,event)">Стільниця</div>
        <div id="cxterminal" class="cmtext" onclick="cmaction(2,event)">Термінал</div>
        <div id="cxfiles" class="cmtext" onclick="cmaction(4,event)">Файли</div>
        <div id="cxevents" class="cmtext" onclick="cmaction(5,event)">Події</div>
        <div id="cxdetails" class="cmtext" onclick="cmaction(6,event)">Деталі</div>
        <div id="cxconsole" class="cmtext" onclick="cmaction(7,event)">Консоль</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">Плаґіни</div>
        <hr id="cxmgroupsplit">
        <div id="cxstar" class="cmtext" onclick="cmaction(10,event)" style="display:none">Перемикач Зірочки</div>
        <div id="cxmdesktop" class="cmtext" onclick="cmaction(9,event)" style="display:none">Мульти Стільниця</div>
    </div>
    <div id="meshContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div id="cxselectall" class="cmtext" onclick="cmmeshaction(1,event)">Обрати Все</div>
        <div id="cxselectnone" class="cmtext" onclick="cmmeshaction(2,event)">Скинути вибір</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)">Замінити Порт</div>
    </div>
    <div id="sshPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmsshportaction(1,event)">Замінити Порт</div>
    </div>
    <div id="rfbPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmrfbportaction(1,event)">Замінити Порт</div>
    </div>
    <div id="httpPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpportaction(1,event)">Замінити Порт</div>
    </div>
    <div id="httpsPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpsportaction(1,event)">Замінити Порт</div>
    </div>
    <div id="filesContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmfilesaction(1,event)">Перейменувати</div>
        <div class="cmtext" onclick="cmfilesaction(2,event)">Редагувати</div>
        <div class="cmtext" onclick="cmfilesaction(3,event)">Видалити</div>
    </div>
    <div id="expandAllContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmexpandaction(1,event)">Розгорнути все</div>
        <div class="cmtext" onclick="cmexpandaction(2,event)">Згорнути Все</div>
    </div>
    <div id="deskPlayerContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskplayeraction(1,event)">Відкрити плеєр...</div>
    </div>
    <div id="deskKeyShortcutContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskshortcutaction(1,event)">Пристосувати...</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)">Пристосувати...</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="Клікніть, щоб переглянути поточні сповіщення" 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>Мої пристрої</span>
                                </div>

                                <div class="userDropdownMenuItem userDropdownMobileOnly mobile-menu-item" data-action="events" onclick="handleUserMenuItem('events')">
                                    <i class="fa fa-calendar userDropdownMenuIcon"></i>
                                    <span>Мій журнал подій</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>Мої користувачі</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>Мої Файли</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>Мій Сервер</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">Перемкнути нічний режим</span>
                                </div>


                                <div class="userDropdownMenuItem" data-action="notes" onclick="handleUserMenuItem('notes')">
                                    <i class="fa fa-sticky-note userDropdownMenuIcon"></i>
                                    <span>Особисті Нотатки</span>
                                </div>

                                <div class="userDropdownMenuItem" data-action="account" onclick="handleUserMenuItem('account')">
                                    <i class="fa fa-user userDropdownMenuIcon"></i>
                                    <span>Мій акаунт</span>
                                </div>

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

                                <div class="userDropdownMenuItem" data-action="logout" onclick="handleUserMenuItem('logout')">
                                    <i class="fa fa-sign-out userDropdownMenuIcon"></i>
                                    <span>Вийти</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)">Мої пристрої</td>
                            <td tabindex="0" id="MainMenuMyAccount" class="topbar_td style3x" onmouseup="go(2,event)" onkeypress="if (event.key == 'Enter') go(2)">Мій акаунт</td>
                            <td tabindex="0" id="MainMenuMyEvents" class="topbar_td style3x" onmouseup="go(3,event)" onkeypress="if (event.key == 'Enter') go(3)">Мій журнал подій</td>
                            <td tabindex="0" id="MainMenuMyFiles" class="topbar_td style3x" onmouseup="go(5,event)" onkeypress="if (event.key == 'Enter') go(5)">Мої Файли</td>
                            <td tabindex="0" id="MainMenuMyUsers" class="topbar_td style3x" onmouseup="go(4,event)" onkeypress="if (event.key == 'Enter') go(4)">Мої користувачі</td>
                            <td tabindex="0" id="MainMenuMyServer" class="topbar_td style3x" onmouseup="go(6,event)" onkeypress="if (event.key == 'Enter') go(6)">Мій Сервер</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)">Загальні</td>
                                        <td tabindex="0" id="MainDevDesktop" class="topbar_td style3x" onmouseup="go(11,event)" onkeypress="if (event.key == 'Enter') go(11)">Стільниця</td>
                                        <td tabindex="0" id="MainDevTerminal" class="topbar_td style3x" onmouseup="go(12,event)" onkeypress="if (event.key == 'Enter') go(12)">Термінал</td>
                                        <td tabindex="0" id="MainDevFiles" class="topbar_td style3x" onmouseup="go(13,event)" onkeypress="if (event.key == 'Enter') go(13)">Файли</td>
                                        <td tabindex="0" id="MainDevEvents" class="topbar_td style3x" onmouseup="go(16,event)" onkeypress="if (event.key == 'Enter') go(16)">Події</td>
                                        <td tabindex="0" id="MainDevInfo" class="topbar_td style3x" onmouseup="go(17,event)" onkeypress="if (event.key == 'Enter') go(17)">Деталі</td>
                                        <td tabindex="0" id="MainDevApps" class="topbar_td style3x" onmouseup="go(18,event)" onkeypress="if (event.key == 'Enter') go(18)">Програмне забезпечення</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)">Консоль</td>
                                        <td tabindex="0" id="MainDevPlugins" class="topbar_td style3x" onmouseup="go(19,event)" onkeypress="if (event.key == 'Enter') go(19)">Плаґіни</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)">Загальні</td>
                                <td tabindex="0" id="MeshSummary" class="topbar_td style3x" onmouseup="go(21,event)" onkeypress="if (event.key == 'Enter') go(21)">Зведення</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)">Події</td>
                                <td tabindex="0" id="EventsReport" class="topbar_td style3x" onmouseup="go(60,event)" onkeypress="if (event.key == 'Enter') go(60)">Звіти</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)">Загальні</td>
                                <td tabindex="0" id="UserEvents" class="topbar_td style3x" onmouseup="go(31,event)" onkeypress="if (event.key == 'Enter') go(31)">Події</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)">Користувачі</td>
                                <td tabindex="0" id="UsersGroups" class="topbar_td style3x" onmouseup="go(50,event)" onkeypress="if (event.key == 'Enter') go(50)">Групи</td>
                                <td tabindex="0" id="UsersRecordings" class="topbar_td style3x" onmouseup="go(52,event)" onkeypress="if (event.key == 'Enter') go(52)">Записи</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)">Загальні</td>
                                <td tabindex="0" id="ServerStats" class="topbar_td style3x" onmouseup="go(40,event)" onkeypress="if (event.key == 'Enter') go(40)">Статистика</td>
                                <td tabindex="0" id="ServerConsole" class="topbar_td style3x" onmouseup="go(115,event)" onkeypress="if (event.key == 'Enter') go(115)">Консоль</td>
                                <td tabindex="0" id="ServerTrace" class="topbar_td style3x" onmouseup="go(41,event)" onkeypress="if (event.key == 'Enter') go(41)">Траса</td>
                                <td tabindex="0" id="ServerPlugins" class="topbar_td style3x" onmouseup="go(42,event)" onkeypress="if (event.key == 'Enter') go(42)">Плаґіни</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">З’єднання з сервером втрачено</span>, <href onclick="reload()" style="cursor:pointer"><u>натисніть для перепідключення</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">Мої пристрої</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="Перейти до першої сторінки" 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="Перейти до попередньої сторінки" 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="Перейти до наступної сторінки" 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="Перейти до останньої сторінки" 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="Стовпці">
                            <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="Перелік">
                            <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="Стільниці">
                            <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="Стільниці">
                            <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="Мапа" 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="Згорнути Все" 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="Розгорнути все" 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="Обрати Все">
                        <input type="button" class="btn btn-primary me-1 btn-sm" id="GroupActionButton" disabled="disabled" value="Групова дія" onclick="groupActionFunction()">
                        <input type="button" class="btn btn-secondary me-1 btn-sm" id="ScrollToTopButton" onclick="onDevicesScroll(true);" value="Прокрутити Догори">
                        <input type="text" id="SearchInput" class="form-control-sm me-1 btn-sm" placeholder="Фільтр" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Фільтр пристроїв за іменем або префіксом: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Префікс ! для виключення, наприклад !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="Фільтр Пристрою">
                            <option value="0" selected="">Усі</option>
                            <option value="1">Онлайн</option>
                            <option value="5">Офлайн</option>
                            <option value="2">Сеанси</option>
                            <option value="3">Зірка</option>
                            <option value="4">Intel® AMT</option>
                            <option value="6">Допомога</option>
                            <option value="7">Теговані</option>
                            <option value="8">Без тегів</option>
                        </select>
                        <div class="form-check" title="Показувати назву операційної системи пристрою"><input class="form-check-input me-1" type="checkbox" id="RealNameCheckBox" onclick="onRealNameCheckBox()"><label class="form-check-label" for="RealNameCheckBox">Назва в ОС</label></div>
                        <label style="display:none"><input type="checkbox" id="OnlineCheckBox" class="form-check-input me-2" onclick="onOnlineCheckBox(event)"><span title="Показувати лише пристрої, що є в мережі">Онлайн</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="Згорнути Все" 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="Розгорнути все" 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="Підключити Все">
                            <input type="button" class="btn btn-danger me-1 btn-sm" onclick="disconnectAllKvmFunction()" value="Відключити Все">
                            <div class="form-check form-check-inline me-1 align-middle" title="Автоматичне підключення">
                                <input class="form-check-input me-1" type="checkbox" id="autoConnectDesktopCheckbox" onclick="autoConnectDesktops(event)">
                                <label class="form-check-label" for="autoConnectDesktopCheckbox">Авто</label>
                            </div>
                        </span>
                        <input type="button" class="btn btn-secondary me-1 btn-sm" onclick="onDevicesScroll(true);" value="Прокрутити Догори">
                        <input type="button" class="btn btn-primary me-1 btn-sm" onclick="showMultiDesktopSettings()" value="Налаштування">
                        <input type="text" id="KvmSearchInput" class="form-control-sm me-1" placeholder="Фільтр" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Фільтр пристроїв за іменем або префіксом: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Префікс ! для виключення, наприклад !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="Пошук розташування" onfocus="onMapSearchFocus(1)" onblur="onMapSearchFocus(0)">
                        <input class="btn btn-primary me-1 btn-sm" type="button" value="Пошук" title="Пошук розташування" onclick="getSearchLocation()">
                        <input class="btn btn-secondary me-1 btn-sm" type="button" id="refreshmap" title="Скинути вигляд мапи" value="Перезапустити" onclick="refreshMap(false,true)">
                    </div>
                    <div class="d-flex align-items-center ms-auto">
                        <div style="display:none" id="devListToolbarView">
                            Вигляд
                            <select id="viewselect" onchange="onDeviceViewChange()">
                                <option value="1">Стовпці</option>
                                <option value="2">Перелік</option>
                                <option value="3">Фіксована Ширина Стільниць</option>
                                <option id="viewselectmapoption" value="4" style="display:none">Мапа</option>
                                <option value="5">Стільниці</option>
                            </select>
                        </div>
                        <div style="display:none" id="devListToolbarSize">
                            Розмір
                            <select id="sizeselect" class="form-select-sm me-2" onchange="onDeviceViewChange()">
                                <option value="0">Мала</option>
                                <option value="1">Середня</option>
                                <option value="2">Велика</option>
                            </select>
                            &nbsp;
                        </div>
                        <div style="display:none" id="devListToolbarSort">
                            Сортування
                            <select id="sortselect" class="form-select-sm me-2" onchange="mainUpdate(6)">
                                <option>Група</option>
                                <option>Живлення</option>
                                <option>Пристрій</option>
                                <option>Теги</option>
                                <option>Теги Груп</option>
                                <option>Востаннє підключено</option>
                                <option>Час останнього запуску</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">Для початку, <a href="#" onclick="return account_createMesh()"><strong>клікніть тут, щоб створити групу пристроїв</strong></a>.</div>
                                <div id="getStarted2">Немає груп пристроїв.</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>Х</b></div>
                            <div style="padding:5px">Результати Локації</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>Мій акаунт</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)">Змінити Світлину</a></div>
                    </div>
                    <div id="p2AccountSecurity" style="display:none">
                        <p><strong>Безпека акаунту</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()">Керувати номером телефону</a><br></span>
                            </div>
                            <div id="manageEmail2FA">
                                <div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthEmail()">Керувати автентифікацією через е-пошту</a><br></span>
                            </div>
                            <div id="manageAuthApp">
                                <div class="p2AccountActions"><span id="authAppSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthApp()">Керувати автентифікацією через застосунок</a><br></span>
                            </div>
                             <div id="manageDuoApp">
                                <div class="p2AccountActions"><span id="authDuoSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthDuo()">Керування автентифікацією 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)">Керувати ключами безпеки</a><br></span>
                            </div>
                            <div id="managePushAuthDev">
                                <div class="p2AccountActions"><span id="authPushAuthDevCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_managePushAuthDev()">Керувати 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()">Керувати повідомленнями</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)">Керувати резервними кодами</a><br></span>
                            </div>
                            <div class="p2AccountActions"></div><span><a href="#" onclick="return account_viewPreviousLogins()">Переглянути попередні спроби входу</a><br></span>
                        </div>
                    </div>
                    <div id="p2AccountActions">
                        <p><strong>Дії з акаунтом</strong></p>
                        <p class="mL">
                            <span id="managePhoneNumber2" style="display:none"><a href="#" onclick="return account_managePhone()">Керувати номером телефону</a><br></span>
                            <span id="manageMessaging2" style="display:none"><a href="#" onclick="return account_manageMessaging()">Керувати повідомленнями</a><br></span>
                            <span id="verifyEmailId" style="display:none"><a href="#" onclick="return account_showVerifyEmail()">Підтвердити е-пошту</a><br></span>
                            <span id="accountEnableNotificationsSpan" style="display:none"><a href="#" onclick="return account_enableNotifications()">Увімкнути вебсповіщення</a><br></span>
                            <a href="#" onclick="return account_showLocalizationSettings()">Параметри Локалізації</a><br>
                            <a href="#" onclick="return account_showAccountNotifySettings()">Налаштування Сповіщень</a><br>
                            <span id="p2AccountPassActions">
                                <span id="accountChangeEmailAddressSpan" style="display:none"><a href="#" onclick="return account_showChangeEmail()">Змінити адресу е-пошти</a><br></span>
                                <a href="#" onclick="return account_showChangePassword()">Змінити пароль</a><span id="p2nextPasswordUpdateTime"></span><br>
                                <a href="#" onclick="return account_showDeleteAccount()">Видалити акаунт</a><br>
                            </span>
                            <span id="accountCreateLoginTokenSpan" style="display:none"><a href="#" onclick="return account_createLoginToken()">Створити токен входу</a><br></span>
                            <a href="#" onclick="return account_showThemesSwitcher()">Змінити тему</a><br>
                            <span id="accountCustomIconsSpan" style="display:none"><a href="#" onclick="return showIconCustomization()">Налаштування значків</a><br></span>
                        </p>
                    </div>
                    <div id="p2logintokens"></div>
                    <strong>Групи Пристроїв</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> Новий</button></span>
                    <br><br>
                    <div id="p2meshes"></div>
                    <div id="p2noMeshFound" style="display:none">Немає груп пристроїв.<span id="p2createMeshLink2"> <a href="#" onclick="return account_createMesh()"><strong>Натисніть, щоб розпочати!</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">Мій журнал подій</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">Фільтр</label>
                                <select id="p3filterevents" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="">Усі Журнали</option>
                                    <option notransval="1" value="agentlog">Журнал Агента</option>
                                    <option notransval="1" value="relaylog">Журнал Ретрансляцій</option>
                                    <option notransval="1" value="manual">Журнал Власноручний</option>
                                    <option notransval="1" value="runcommands">Журнал Запуску Команд</option>
                                    <option notransval="1" value="batchupload">Журнал Пакетного Вивантаження</option>
                                    <option notransval="1" value="changenode">Журнал Зміни Вузлів</option>
                                    <option notransval="1" value="removenode">Журнал Видалення Вузлів</option>
                                </select>
                                <label for="p3limitdropdown" class="me-1">Показати</label>
                                <select id="p3limitdropdown" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="60">Останні 60</option>
                                    <option notransval="1" value="120">Останні 120</option>
                                    <option notransval="1" value="250">Останні 250</option>
                                    <option notransval="1" value="500">Останні 500</option>
                                    <option notransval="1" value="1000">Останні 1000</option>
                                    <option notransval="1" value="">Без обмежень</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(2)"><i class="fa-solid fa-download" title="Отримати Події"></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">Мої користувачі</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="Обрати Все">
                                <input type="button" id="UsersGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Групова дія" onclick="p3usersGroupActionFunction()">
                                <input id="UserNewAccountButton" class="btn btn-success btn-sm me-1" type="button" onclick="showCreateNewAccountDialog()" value="Новий Акаунт...">
                                <div class="btn-group me-1 mobileActionDropdown">
                                    <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">Більше&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
                                    <div class="dropdown-menu">
                                        <button type="button" class="dropdown-item" onclick="showUserBroadcastDialog()">Розсилка</button>
                                        <button type="button" class="dropdown-item" onclick="p4downloadUserInfo()">Отримати інформацію про користувача</button>
                                        <button type="button" id="p4MobileUserBatchCreate" class="dropdown-item" style="display:none" onclick="p4batchAccountCreate()">Пакетне створення облікових записів</button>
                                        <button type="button" class="dropdown-item" onclick="onUsersViewSettings()">Налаштування</button>
                                    </div>
                                </div>
                                <div class="d-inline-flex align-items-center">
                                    <input id="UserSearchInput" type="search" class="form-control-sm me-1" placeholder="Фільтр" 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="Розсилка">
                                <a href="#" onclick="p4downloadUserInfo()"><i role="button" class="fa-solid fa-download me-2" title="Отримати інформацію про користувача"></i></a>
                                <a href="#" onclick="p4batchAccountCreate()"><i role="button" id="p4UserBatchCreate" style="display:none" title="Пакетне створення кількох акаунтів користувачів" 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="Налаштування" 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">Мої Файли</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="Вгору">
                                <input type="button" id="p5SelectAllButton" class="btn btn-secondary btn-sm me-1" disabled="disabled" onclick="p5selectallfile();" value="Обрати Все">
                                <input type="button" id="p5RenameFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Перейменувати" onclick="p5renamefile();">
                                <input type="button" id="p5DeleteFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Видалити" onclick="p5deletefile();">
                                <input type="button" id="p5ViewFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Редагувати" onclick="p5viewfile()">
                                <input type="button" id="p5NewFolderButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Нова Тека" onclick="p5createfolder();">
                                <input type="button" id="p5UploadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Відвантажити" onclick="p5uploadFile()">
                                <input type="button" id="p5DownloadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Завантажити" onclick="p5downloadButton()">
                                <input type="button" id="p5CutButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Вирізати" onclick="p5copyFile(1)">
                                <input type="button" id="p5CopyButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Копіювати" onclick="p5copyFile(0)">
                                <input type="button" id="p5PasteButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Вставити" 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">Більше&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()">Нова Тека</button>
                                        <button type="button" disabled="disabled" id="p5MobileUploadButton" class="dropdown-item" onclick="p5uploadFile()">Відвантажити</button>
                                        <button type="button" disabled="disabled" id="p5MobileDownloadButton" class="dropdown-item" onclick="p5downloadButton()">Завантажити</button>
                                        <button type="button" disabled="disabled" id="p5MobileCutButton" class="dropdown-item" onclick="p5copyFile(1)">Вирізати</button>
                                        <button type="button" disabled="disabled" id="p5MobileCopyButton" class="dropdown-item" onclick="p5copyFile(0)">Копіювати</button>
                                        <button type="button" disabled="disabled" id="p5MobilePasteButton" class="dropdown-item" onclick="p5pasteFile()">Вставити</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">Сортувати за назвою</option>
                                    <option value="2">Сортувати за розміром</option>
                                    <option value="3">Сортувати за датою</option>
                                    <option value="4">Сортувати по назві за спаданням</option>
                                    <option value="5">Сортувати по розміру за спаданням</option>
                                    <option value="6">Сортувати по даті за спаданням</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>Ці файли доступні для всіх, клікніть "посилання", щоб отримати публічну URL-адресу.</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">Мій Сервер</div>
                        <img id="MainMeshImage" src="serverpic.ashx" class="ms-auto">
                    </div>
                    <div id="p2ServerActions">
                        <p><strong>Дії сервера</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()">Завантажте Резервну Копію Сервера</a>
                            </div>
                            <div id="p2ServerActionsRestore">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showRestoreDlg()">Відновити сервер із резервної копії</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()">Резервне копіювання 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()">Перевірити наявність оновлень</a>
                            </div>
                            <div id="p2ServerActionsErrors">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showErrorsDlg()">Показати журнал помилок сервера</a>
                            </div>
                            <div id="p2ServerActionsConfig">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showConfigDlg()">Показати конфігурацію сервера</a>
                            </div>
                        </div>
                        <br>
                    </div>
                    <strong>Статистика сервера</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>Попередження сервера</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="Назад" 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>Загальні<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>Примітки</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="Зірка" 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="Перелік віддалених сеансів, активних на цьому комп'ютері." 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="Запит Допомоги" 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="Назад" 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>Стільниця<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">SCROLL</div>
                    </div>
                    <div id="devListToolbarViewIcons">
                        <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick="deskToggleFull(event)" title="Повноекранний. Утримуйте Shift для перегляду на весь екран."></i>
                    </div>
                </div>
                <div id="p11warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Функцію переадресації портів або KVM вимкнено<span id="p11warninga">, клікнути тут, щоб увімкнути це.</span></div>
                </div>
                <div id="p11warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Віддалений комп'ютер не ввімкнено, клікніть тут, щоб надіслати команду увімкнення.
                    </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="АвтоПідключення" 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="Підключитися до віддаленої стільниці за допомогою MeshAgent" onclick="connectDesktop(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Підключитись</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">Перемкнути спадний список</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(1,event)">Запитати дозвіл + панель підключення</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(2,event)">Запитати дозвіл</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(3,event)">Панель підключення</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="Підключитися за допомогою RDP" onclick="askRdpCredentials()" onkeypress="return false" onkeydown="return false" disabled="disabled">Підключення через RDP</button>
                                <button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" id="connectbutton1rd" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">Перемкнути спадний список</span></button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmaltportaction(1,event)">Замінити Порт</a></li>
                                </ul>
                            </div>
                            <span id="connectbutton1hspan"><button class="btn btn-primary btn-sm me-1" type="button" id="connectbutton1h" title="Підключитися за допомогою апаратного KVM" onclick="connectDesktop(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">Підключення Апаратне</button></span>
                            <div class="btn-group dropdown-center me-1" id="disconnectbutton1span">
                                <button type="button" class="btn btn-danger btn-sm" id="disconnectbutton1" title="Підключитися до віддаленої стільниці за допомогою MeshAgent" onclick="connectDesktop(event,0)" onkeypress="return false" onkeydown="return false">Відключити</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">Перемкнути спадний список</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(10,event)">Відключити та заблокувати</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(11,event)">Відключити</a></li>
                                </ul>
                            </div>
                            <span id="deskstatus" style="line-height:22px">Відключено</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="Змінити стан електроживлення віддаленої машини" onkeypress="return false" onkeydown="return false" value="Керування живленням..." onclick="showPowerActionDlg()" style="display:none">
                            <input id="deskActionsBtn" type="button" class="btn btn-primary btn-sm me-1" title="Увімкнути живлення на пристрої" onkeypress="return false" onkeydown="return false" value="Дії" onclick="deviceActionFunction()">
                            <input id="deskActionsSettings" type="button" class="btn btn-primary btn-sm me-1" value="Налаштування" title="Редагувати параметри віддаленої стільниці" onkeypress="return false" onkeydown="return false" onclick="showDesktopSettings()">
                            <input id="deskFocusBtn" type="button" title="Увімкнути режим фокусування, коли він активний, оновлюється лише область навколо миші" onkeypress="return false" onkeydown="return false" value="Зфокусувати все" onclick="deskToggleFocus()" style="margin-right:3px;display:none">
                            <div id="deskRecordIcon" class="deskareaicon" title="Сервер записує цей сеанс" style="display:none;background-color:red;width:12px;height:12px;border-radius:6px;margin-top:5px"></div>
                            <div class="deskareaicon" title="Прокрутити праворуч" onclick="drotate(1)"><i class="fa-solid fa-rotate-right"></i></div>
                            <div class="deskareaicon" title="Прокрутити ліворуч" onclick="drotate(-1)"><i class="fa-solid fa-rotate-left"></i></div>
                            <div class="deskareaicon" title="Перемкнути Режим Перегляду" 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()">Оновити</a>
                                <div id="deskToolsTopTabProcess" class="deskToolsTopTab" onclick="changeDeskToolTab(0)" style="left:0px;bottom:0px">Процеси</div>
                                <div id="deskToolsTopTabService" class="deskToolsTopTab" onclick="changeDeskToolTab(1)" style="display:none;left:90px;color:gray">Послуги</div>
                            </div>
                            <div id="deskToolsArea">
                                <div id="DeskToolsProcessTab">
                                    <div id="deskToolsHeader">
                                        <a class="colmn1" title="Сортувати за ідентифікатором процесу" onclick="sortProcess(0)">PID</a>
                                        <a class="colmn2" title="Сортувати за назвою" onclick="sortProcess(1)">Ім'я</a>
                                    </div>
                                    <div id="DeskToolsProcesses" style=""></div>
                                </div>
                                <div id="DeskToolsServiceTab" style="display:none">
                                    <div id="deskToolsServiceHeader">
                                        <a class="colmn1" style="width:70px" title="Сортувати за країною" onclick="sortService(0)">Стан</a>
                                        <a class="colmn2" title="Сортувати за назвою" onclick="sortService(1)">Ім'я</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()">Надіслати</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="Буфер Обміну" onkeypress="return false" onkeydown="return false" onclick="showDeskClip()">
                            <input id="DeskType" class="btn btn-secondary btn-sm me-1" cmenu="deskPreConfigShortcutContextMenu" type="button" value="Тип" 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="Показати/приховати екранну клавіатуру" style="display:none">
                                <i class="fa-solid fa-keyboard"></i>
                            </button>
                            <div id="DeskControlSpan" title="Змінити введення даних за допомогою миші та клавіатури" 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">Введення</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="Більше дій">
                                Більше&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="Затримка Сесії Стільниці"></span>
                            <span id="DeskTimer" style="width:70px" class="text-center" title="Час сеансу"></span>
                            <input id="DeskToolsButton" type="button" class="btn btn-primary btn-sm mx-1" value="Засоби" title="Показати/сховати засоби" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()">
                            <span id="DeskGuestShareButton" title="Поширити доступ до пристрою з гостем" role="button">
                                <i class="fa-solid fa-fw fa-share-nodes" id="DeskInputUnLockedButtonImage" onclick="showShareDevice()"></i>
                            </span>
                            <span id="DeskInputUnLockedButton" title="Віддалене введення розблоковано" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" onclick="deskInputLockFunction(1)"></i>
                            </span>
                            <span id="DeskInputLockedButton" title="Віддалене введення заблоковано" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" style="color:red" onclick="deskInputLockFunction(0)"></i>
                            </span>
                            <span id="DeskRefreshButton" title="Оновити стільницю" role="button">
                                <i class="fa-solid fa-fw fa-rotate" onclick="deskRefreshFunction()"></i>
                            </span>
                            <span id="DeskClipboardOutButton" title="Передати локальний буфер обміну на віддалений пристрій" 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="Завантажити віддалений буфер у локальний буфер обміну" 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="Записати сеанс віддаленої стільниці у файл" style="display:none" role="button">
                                <i class="fa-solid fa-fw fa-video" onclick="deskRecordSession()" id="DeskRecordButtonImage"></i>
                            </span>
                            <span id="DeskSaveImageButton" title="Зберегти знімок екрана віддаленої стільниці" role="button">
                                <i class="fa-solid fa-fw fa-camera" onclick="deskSaveImage()"></i>
                            </span>
                            <span id="DeskBackgroundButton" title="Перемкнути тло віддаленої стільниці" role="button">
                                <i class="fa-solid fa-fw fa-image" onclick="deviceToggleBackground(event)" id="DeskBackgroundButtonImage"></i>
                            </span>
                            <span id="DeskOpenWebButton" title="Відкрити вебадресу на віддаленому комп'ютері" role="button">
                                <i class="fa-solid fa-fw fa-globe" onclick="deviceUrlFunction()"></i>
                            </span>
                            <span id="DeskLockButton" title="Заблокувати віддалений комп'ютер" role="button">
                                <i class="fa-solid fa-fw fa-lock" onclick="deviceLockFunction()"></i>
                            </span>
                            <span id="DeskNotifyButton" title="Показ сповіщення на віддаленому комп'ютері" role="button">
                                <i class="fa-solid fa-fw fa-bell" onclick="deviceToastFunction()"></i>
                            </span>
                            <span id="DeskChatButton" class="deskarea" title="Відкрити вікно чату на цьому комп'ютері" role="button">
                                <i class="fa-solid fa-fw fa-message" onclick="deviceChat(event)"></i>
                            </span>
                            <span id="DeskRunButton" cmenu="deskPreConfigScriptContextMenu" class="deskarea" title="Запустити сценарій на цьому комп'ютері" 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>Вибрати монітор<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>Екранна клавіатура</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>Ввести текст</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>Дії</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>Налаштування</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>Увімкнути режим тачпада</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>Увімкнути режим правої кнопки миші</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>Налаштування ярликів</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>Оновити робочий стіл</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>Буфер Обміну</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>Завантажити буфер обміну</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>Відкрити URL на віддаленому комп'ютері</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>Заблокувати віддалений комп'ютер</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>Надіслати сповіщення</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>Чат</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>Запустити скрипт</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="Назад" 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>Термінал<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="Повноекранний. Утримуйте Shift для перегляду на весь екран."></i>
                    </div>
                </div>
                <div id="p12warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Функцію переадресації портів або KVM вимкнено<span id="p12warninga">, клікнути тут, щоб увімкнути це.</span></div>
                </div>
                <div id="p12warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Віддалений комп'ютер не ввімкнено, клікніть тут, щоб надіслати команду увімкнення.
                </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>
                                        АвтоПідключення</button>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2span">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2" title="Підключитися до віддаленої стільниці за допомогою MeshAgent" onclick="connectTerminal(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Підключитись</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">Перемкнути спадний список</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Консоль від імені адміністратора</b></a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0,event)">PowerShell від імені адміністратора</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Консоль користувача</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0,event)">PowerShell користувача</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0x10,event)">Запит на консоль від імені адміністратора</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0x10,event)">Запит на PowerShell від імені адміністратора</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0x10,event)">Запит на консоль користувача</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0x10,event)">Запит на PowerShell користувача</a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(8,0,event)"><b>Консоль користувача</b></a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(9,0,event)">PowerShell користувача</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Root Консоль</b></a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Консоль користувача</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(100,0,event)">Оболонка входу</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="Підключитися до віддаленої стільниці за допомогою MeshAgent" onclick="connectTerminal(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Підключитися до 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">Перемкнути спадний список</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Замінити Порт</a></li>
                                        </ul>
                                    </div>
                                    <span id="connectbutton2hspan"><button type="button" class="btn btn-success btn-sm me-1" id="connectbutton2h" title="Підключення за допомогою апаратного KVM Intel&amp;reg; AMT" onclick="connectTerminal(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">Підключення Апаратне</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">Відключити</button></span>
                                    <span id="termstatus" style="line-height:22px">Відключено</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="Сервер записує цей сеанс" 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="Увімкнути живлення на пристрої" onkeypress="return false" onkeydown="return false" value="Дії" 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="Вкладка" 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="Backspace" 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="Вставити" title="Вставити текст у термінал" onclick="showTermPasteDialog()" style="display:none">
                                </div>
                                <div class="d-flex align-items-center ms-auto">
                                    <span id="TermTimer" title="Час сеансу"></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="Перемкніть, що надсилає клавіша Enter" onclick="termToggleCr()">
                                        <input id="id_tfxkeysbutton" type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" value="Intel (F10 = ESC+[OM)" title="Перемкнути тип емуляції клавіш F1 на 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" title="Перемкнути тип емуляції терміналу" onclick="termToggleType()">
                                    </span>
                                    <span id="terminalSizeDropDown" style="display:none">
                                        <select id="termSizeList" onkeypress="return false" class="form-select-sm me-1">
                                            <option value="1">80x25</option>
                                            <option value="2">100x30</option>
                                        </select>
                                    </span>
                                    <select id="specialkeylist" class="form-select-sm me-1" onkeypress="return false"></select>
                                    <input id="specialkeylistinput" type="button" onkeypress="return false" class="btn btn-primary btn-sm me-1" value="Надіслати" title="Надіслати вибрану спеціальну клавішу" 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="Назад" 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>Файли<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> АвтоПідключення</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connect" title="Підключитися до віддаленої стільниці за допомогою MeshAgent" onclick="connectFiles(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Підключитись</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">Перемкнути спадний список</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmconnectfilesaction()">Запитати дозвіл</a></li>
                                    </ul>
                                </div>
                                <button id="p13Disconnect" class="btn btn-danger btn-sm me-1" onclick="connectFiles(event)" type="button">Відключити</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectsspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connects" title="Підключитися до віддаленої стільниці за допомогою MeshAgent" onclick="connectFiles(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">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">Перемкнути спадний список</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Замінити Порт</a></li>
                                    </ul>
                                </div>
                                <span id="p13Status">Відключено</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="Увімкнути живлення на пристрої" value="Дії" onclick="deviceActionFunction()">
                                <div id="filesRecordIcon" class="deskareaicon" title="Сервер записує цей сеанс" 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="Вгору" onclick="p13folderup()">
                                <input type="button" disabled="disabled" id="p13SelectAllButton" class="btn btn-secondary me-1 btn-sm" value="Обрати Все" onclick="p13selectallfile()">
                                <input type="button" disabled="disabled" id="p13RenameFileButton" class="btn btn-primary me-1 btn-sm" value="Перейменувати" onclick="p13renamefile()">
                                <input type="button" disabled="disabled" id="p13DeleteFileButton" class="btn btn-primary me-1 btn-sm" value="Видалити" onclick="p13deletefile()">
                                <input type="button" disabled="disabled" id="p13ViewFileButton" class="btn btn-primary me-1 btn-sm" value="Редагувати" onclick="p13viewfile()">
                                <input type="button" disabled="disabled" id="p13NewFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Нова Тека" onclick="p13createfolder()">
                                <input type="button" disabled="disabled" id="p13NewFileButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Новий Файл" onclick="p13createfile()">
                                <input type="button" disabled="disabled" id="p13UploadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Відвантажити" onclick="p13uploadFile()">
                                <input type="button" disabled="disabled" id="p13DownloadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Завантажити" onclick="p13downloadButton()">
                                <input type="button" disabled="disabled" id="p13CutButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Вирізати" onclick="p13copyFile(1)">
                                <input type="button" disabled="disabled" id="p13CopyButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Копіювати" onclick="p13copyFile(0)">
                                <input type="button" disabled="disabled" id="p13PasteButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Вставити" 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="Розпакувати" onclick="p13unzipFile()">
                                <input type="button" disabled="disabled" id="p13RefreshButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Оновити" onclick="p13folderup(9999)">
                                <input type="button" disabled="disabled" id="p13FindButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Знайти" onclick="p13findfile()">
                                <input type="button" disabled="disabled" id="p13GoToFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Перейти" onclick="p13gotofolder()">
                                <input type="button" disabled="disabled" id="p13OpenButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Відкрити" 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">Більше&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()">Нова Тека</button>
                                        <button type="button" disabled="disabled" id="p13MobileNewFileButton" class="dropdown-item" onclick="p13createfile()">Новий Файл</button>
                                        <button type="button" disabled="disabled" id="p13MobileUploadButton" class="dropdown-item" onclick="p13uploadFile()">Відвантажити</button>
                                        <button type="button" disabled="disabled" id="p13MobileDownloadButton" class="dropdown-item" onclick="p13downloadButton()">Завантажити</button>
                                        <button type="button" disabled="disabled" id="p13MobileCutButton" class="dropdown-item" onclick="p13copyFile(1)">Вирізати</button>
                                        <button type="button" disabled="disabled" id="p13MobileCopyButton" class="dropdown-item" onclick="p13copyFile(0)">Копіювати</button>
                                        <button type="button" disabled="disabled" id="p13MobilePasteButton" class="dropdown-item" onclick="p13pasteFile()">Вставити</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()">Розпакувати</button>
                                        <button type="button" disabled="disabled" id="p13MobileRefreshButton" class="dropdown-item" onclick="p13folderup(9999)">Оновити</button>
                                        <button type="button" disabled="disabled" id="p13MobileFindButton" class="dropdown-item" onclick="p13findfile()">Знайти</button>
                                        <button type="button" disabled="disabled" id="p13MobileGoToFolderButton" class="dropdown-item" onclick="p13gotofolder()">Перейти</button>
                                        <button type="button" disabled="disabled" id="p13MobileOpenButton" class="dropdown-item" onclick="p13openfilefolder()">Відкрити</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">Сприйнятний для Людини</option>
                                    <option value="1" title="Байти">Байти</option>
                                    <option value="10" title="кБ">Кілобайти</option>
                                    <option value="20" title="МБ">Мегабайти</option>
                                    <option value="30" title="GB">Гігабайт</option>
                                </select>
                                <select id="p13sortdropdown" class="form-select-sm me-2" onchange="p13updateFiles()">
                                    <option value="1" selected="selected">Сортувати за назвою</option>
                                    <option value="2">Сортувати за розміром</option>
                                    <option value="3">Сортувати за датою</option>
                                    <option value="4">Сортувати по назві за спаданням</option>
                                    <option value="5">Сортувати по розміру за спаданням</option>
                                    <option value="6">Сортувати по даті за спаданням</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="Назад" 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="Повноекранний. Утримуйте Shift для перегляду на весь екран."></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="Назад" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Консоль<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="Інформація про поточне ядро, що працює на цьому агенті"></div>
                                <input type="button" id="p15uploadCore" class="btn btn-primary me-1 btn-sm" value="Дії над агентом" onclick="p15uploadCore(event)" title="Змініть модуль коду JavaScript агента">
                                <i onclick="p15downloadConsoleText()" title="Отримати текст консолі" 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">Агент</option>
                                            <option id="p15outputselect3" value="3">Push</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="Очистити" 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="Назад" 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>Події<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">Фільтр</label>
                                <select id="p16filterevents" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="">Усі Журнали</option>
                                    <option notransval="1" value="agentlog">Журнал Агента</option>
                                    <option notransval="1" value="relaylog">Журнал Ретрансляцій</option>
                                    <option notransval="1" value="manual">Журнал Власноручний</option>
                                    <option notransval="1" value="runcommands">Журнал Запуску Команд</option>
                                    <option notransval="1" value="batchupload">Журнал Пакетного Вивантаження</option>
                                    <option notransval="1" value="changenode">Журнал Зміни Вузлів</option>
                                    <option notransval="1" value="removenode">Журнал Видалення Вузлів</option>
                                </select>
                                <label class="me-1" for="p16limitdropdown">Показати</label>
                                <select id="p16limitdropdown" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="60">Останні 60</option>
                                    <option notransval="1" value="120">Останні 120</option>
                                    <option notransval="1" value="250">Останні 250</option>
                                    <option notransval="1" value="500">Останні 500</option>
                                    <option notransval="1" value="1000">Останні 1000</option>
                                </select>
                                <i onclick="p3showDownloadEventsDialog(1)" class="fa-solid fa-download" title="Отримати Події" 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="Назад" 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>Деталі<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="Оновіть детальну інформацію."></i>
                        <i class="fa-solid fa-chart-line fa-xl fa-border" role="button" onclick="deskToggleCpuGraph(event)" title="Показати процесор і використання пам'яті пристрою."></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>Живий Графік</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="Назад" 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>Програмне забезпечення<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="Оновити" onclick="loadInstalledApps()">
                        <input type="text" id="p18SearchInput" class="form-control-sm" placeholder="Пошук програмного забезпечення..." style="width:250px" onkeyup="filterInstalledApps()">
                        <label style="margin-left:15px">
                            <input type="checkbox" id="p18ShowStore" onchange="toggleStoreApps()"> Показати додатки магазину
                        </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>Натисніть Оновити для завантаження встановленого ПЗ...</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="Назад" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Плаґіни<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="Назад" onkeypress="if (event.key == 'Enter') goBack()"></i>
                        </div>
                        <div class="fs-4 fw-bold">Загальні<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="Назад" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Резюме - <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">Стани Живлення</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">Тип Агента</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">З'єднання</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">Безпека</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="Назад" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Загальні<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="Назад" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Події<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">Фільтр</label>
                                <select id="p31filterevents" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="">Усі Журнали</option>
                                    <option notransval="1" value="agentlog">Журнал Агента</option>
                                    <option notransval="1" value="relaylog">Журнал Ретрансляцій</option>
                                    <option notransval="1" value="manual">Журнал Власноручний</option>
                                    <option notransval="1" value="runcommands">Журнал Запуску Команд</option>
                                    <option notransval="1" value="batchupload">Журнал Пакетного Вивантаження</option>
                                    <option notransval="1" value="changenode">Журнал Зміни Вузлів</option>
                                    <option notransval="1" value="removenode">Журнал Видалення Вузлів</option>
                                </select>
                                <label for="p31limitdropdown" class="me-1">Показати</label>
                                <select id="p31limitdropdown" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="60">Останні 60</option>
                                    <option notransval="1" value="120">Останні 120</option>
                                    <option notransval="1" value="250">Останні 250</option>
                                    <option notransval="1" value="500">Останні 500</option>
                                    <option notransval="1" value="1000">Останні 1000</option>
                                    <option notransval="1" value="">Без обмежень</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(3)"><i class="fa-solid fa-download" role="button" title="Отримати Події"></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">Статистика Мого Сервера</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Оновити" type="button" class="btn btn-primary btn-sm me-2" onclick="refreshServerTimelineStats()">
                        <div class="form-check"><input class="form-check-input me-1" type="checkbox" id="p40log" onclick="updateServerTimelineHours()"><label class="form-check-label" for="p40log">Log-X</label></div>
                    </div>
                    <div class="toright2 d-flex align-items-center ms-auto">
                        <select id="p40server" style="display:none" onchange="updateServerTimelineStats()"></select>
                        <select id="p40type" class="form-select-sm me-1" onchange="updateServerTimelineStats()">
                            <option value="0">Підключення</option>
                            <option value="1">Пам'ять</option>
                            <option value="5">ЦП</option>
                            <option value="3">Вхідний Трафік</option>
                            <option value="4">Вихідний Трафік </option>
                        </select>
                        <select id="p40time" class="form-select-sm me-1" onchange="updateServerTimelineHours()">
                            <option value="3">Останні 3 години</option>
                            <option value="8">Останні 8 годин</option>
                            <option value="24">Останній день</option>
                            <option value="168">Останній тиждень</option>
                            <option value="720">Останні 30 днів</option>
                        </select>
                        <i class="fa-solid fa-download" title="Завантажити точки даних (.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">Трасування Мого Сервера</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Трасування" type="button" class="btn btn-primary btn-sm me-1" onclick="setServerTracing()">
                        <span id="p41traceStatus">Немає</span>
                    </div>
                    <div class="toright2 d-flex align-items-center ms-auto">
                        <label for="p41limitdropdown" class="me-1">Показати</label>
                        <select id="p41limitdropdown" class="form-select-sm me-1" onchange="displayServerTrace()">
                            <option value="100">Останні 100</option>
                            <option value="250">Останні 250</option>
                            <option value="500">Останні 500</option>
                            <option value="1000">Останні 1000</option>
                        </select>
                        <input value="Очистити" type="button" class="btn btn-primary btn-sm me-1" onclick="clearServerTracing()">
                        <i class="fa-solid fa-download" title="Завантажити трасування (.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">Плаґіни Мого Сервера</div>
                </div>
                <div class="areaHead">
                    <div class="toright2">
                    </div>
                    <div>
                        <input value="Завантажити Плаґін" type="button" onclick="return pluginHandler.addPluginDlg();">
                    </div>
                </div>
                <div id="pluginRestartNotice" class="areaHead" style="background-color:gold;display:none">
                    <div class="toright2">
                        <input value="Оновити ядра Агента" type="button" onclick="distributeCore();return false">
                    </div>
                    <div style="padding:2px">
                        <div style="padding:2px"><b>Повідомлення:</b> Плаґіни було змінено, що може вимагати оновлення основного ядра агента.</div>
                    </div>
                </div>
                <table id="p42tbl">
                    <tbody><tr class="DevSt">
                        <th style="width:26px"></th>
                        <th style="width:10px"></th>
                        <th class="chName">Ім'я</th>
                        <th class="chDescription">Опис</th>
                        <th class="chSite" style="text-align:center">Лінк</th>
                        <th class="chVersion" style="text-align:center">Версія</th>
                        <th class="chUpgradeAvail" style="text-align:center">Остання</th>
                        <th class="chStatus" style="text-align:center">Стан</th>
                        <th class="chAction" style="text-align:center">Дія</th>
                        <th style="width:10px"></th>
                    </tr>
                </tbody></table>
                <div id="pluginNoneNotice" style="width:100%;text-align:center;padding-top:10px;display:none"><i>На сервері немає плаґінів.</i></div>
            </div>
            <div id="p43" style="display:none">
                <div id="p43BackButton">
                    <div class="backButton" tabindex="0" onmouseup="go(42)" title="Назад" onkeypress="if (event.key == 'Enter') go(42)">
                        <div class="backButtonEx"></div>
                    </div>
                </div>
                <h1>Плаґіни Мого Сервера - <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">Групи Мого Користувача</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="Обрати Все">
                                <input type="button" id="UsersGroupsGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Групова дія" onclick="p50usersGroupActionFunction()">
                                <input id="NewUserGroupButton" type="button" class="btn btn-success btn-sm me-1" onclick="showCreateUserGroupDialog(1)" value="Нова група...">
                                <input id="DuplicateUserGroupButton" type="button" class="btn btn-primary button-outline-success btn-sm me-1" style="display:none" onclick="showCreateUserGroupDialog(2)" value="Дублювати Групу...">
                            </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="Назад" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Група користувачів - <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">Записи Мого користувача</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="Оновити">
                            </div>
                            <div class="d-flex align-items-center ms-auto">
                                <input type="button" class="btn btn-primary btn-sm" onclick="openRecodringPlayer()" value="Відкрити плеєр...">
                            </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">Мої Звіти</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="Завантажити Звіт"></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> Згенерувати звіт...</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()">Підтвердити е-пошту</a>
                        &nbsp;<a id="termsLinkFooter" href="terms">Умови та конфіденційність</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>Вибір файлу</div>
                                <select id="d3uploadMode" onchange="d3modechange()">
                                    <option value="1">Локальне завантаження файлу</option>
                                    <option value="2">Вибір файлів сервера</option>
                                </select>
                            </div>
                            <div id="d3localmode" style="display:none">
                                <div>Відвантажити файл</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="Вгору">&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="Перенесення увімкнуто" onclick="d4ToggleWrap()">
                            <input id="d4SizeButton" class="btn btn-primary me-1 mb-1" type="button" value="Мала" onclick="d4ToggleSize()">
                            <input id="d4EncodingButton" class="btn btn-primary me-1 mb-1" type="button" value="Необроблене" 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')">Агент</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">Якість</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">Масштабування</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7framelimiter" class="form-select">
                                        <option selected="selected" value="50">Висока</option>
                                        <option value="100">Середня</option>
                                        <option value="400">Низька</option>
                                        <option value="1000">Дуже низька</option>
                                    </select>
                                    <label for="d7framelimiter">Частота кадрів</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7encoding" class="form-select">
                                        <option value="1">JPEG</option>
                                        <option value="2">PNG</option>
                                        <option value="3">TIFF</option>
                                        <option selected="selected" value="4">WEBP</option>
                                    </select>
                                    <label for="d7encoding">Кодування</label>
                                </div>
                                <fieldset id="d7desktopOtherSettings">
                                    <legend>Інші налаштування</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">Поміняти Кнопки Миші</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskrmw" class="form-check-label">Зворотня прокрутка коліщатка миші</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskRemoteKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskRemoteKeyMap" class="form-check-label">Використовувати розкладку віддаленої клавіатури</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">Автоматичний буфер обміну</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">Заблокувати при відключенні</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, Найшвидше</option>
                                        <option value="2">RLE16, Рекомендовано</option>
                                        <option value="3">RAW8, Повільне</option>
                                        <option value="4">RAW16, Дуже Повільне</option>
                                    </select>
                                    <label for="d7desktopmode">Кодування Зображення</label>
                                </div>
                                <fieldset>
                                    <legend>Інші налаштування</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">Показати засіб фокусування</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7showcursor" class="form-check-input me-2" type="checkbox">
                                            <label for="d7showcursor" class="form-check-label">Показати локальний курсор миші</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7localKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7localKeyMap" class="form-check-label">Локальна розкладка клавіатури</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7kvmrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7kvmrmw" class="form-check-label">Зворотня прокрутка коліщатка миші</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">За розміром Полотна</option>
                                        <option notransval="1" value="browser">За розміром Браузера</option>
                                        <option notransval="1" value="screen">За розміром екрана</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">Розмір дисплея</label>
                                </div>
                                <fieldset>
                                    <legend>Параметри</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">Вимкнути Шпалери</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp2" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp2" class="form-check-label">Вимкнути відображення вмісту вікна під час перетягування</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp3" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp3" class="form-check-label">Вимкнути Анімацію Меню</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp4" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp4" class="form-check-label">Вимкнути Візуальну Тему</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp6" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp6" class="form-check-label">Вимкнути Тінь Курсора</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp7" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp7" class="form-check-label">Вимкнути Налаштування Курсору</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp8" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp8" class="form-check-label">Enable Font Smoothing</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp9" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp9" class="form-check-label">Enable Desktop Composision</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpclip" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpclip" class="form-check-label">Автоматичний буфер обміну</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpsmb" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpsmb" class="form-check-label">Поміняти Кнопки Миші</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdprmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdprmw" class="form-check-label">Зворотня прокрутка коліщатка миші</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">Скасувати</button>
                        <button type="button" class="btn btn-primary" id="idx_dlgOkButton">OK</button>
                        <button type="button" class="btn btn-danger" id="idx_dlgDeleteButton" style="display:none">Видалити</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 = ['', "Увімкнено", "Сон", "Сон", "Сон", "Гібернація", "Вимкнути", "Поточний"];
        var StatusStrs = ["Відключено", "Підключення...", "Установити...", "Підключено", "Intel&reg; AMT Підключено"];
        var agentsStr = ["Невідомо", "Консоль Windows 32bit", "Консоль Windows 64bit", "Сервіс Windows 32bit", "Сервіс Windows 64bit", "Linux 32-біт", "Linux 64-біт", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86 (32-біт)", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86 32-біт", "macOS x86-64", "ChromeOS", "Linux Poky x86_64 64-біт", "Linux NoKVM x86 32-біт", "Linux NoKVM x86_64 64-біт", "консоль Windows MinCore", "служба Windows MinCore", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64-біт", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Процесори Apple Silicon", "FreeBSD x86-64", "Невідомо", "Linux ARM 64 біт", "Alpine Linux x86_64 64-біт (MUSL)", "Асистент (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Невідомо", "Невідомо", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Консоль Windows ARM 64bit", "Сервіс Windows ARM 64-біт", "ARMVIRT32 (OpenWRT)", "RISC-V 64bit"];
        var agentsStrNoAgent = ['', '', '', '', "Windows", '', "Linux", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', "Apple"];
        var domainStates = ["Немає", "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]|Набір");

            // 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>' + "Типовий Браузер" + '</label><br>';
            x += '<input type=radio id=night2 name=nightmoderadio value=2 ' + ((cNightMode == 2) ? 'checked' : '') + '><label for=night2>' + "Світлий режим" + '</label><br>';
            x += '<input type=radio id=night1 name=nightmoderadio value=1 ' + ((cNightMode == 1) ? 'checked' : '') + '><label for=night1>' + "Темний режим" + '</label><br>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Нічний Режим", 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', "Інтерфейс користувача", 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', "Не вдалося автентифікувати"); return; }
                if (errorCode == 'invalidorigin') { QH('p0span', "Хибна адреса у HTTP запиті"); return; }
                if (prevState == 2) { if (autoReconnect) { setTimeout(serverPoll, 5000); } } else { QH('p0span', "Неможливо підключитись до вебсокет"); }
                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: "Будь ласка, додайте двофакторні резервні коди. Якщо поточну 2FA буде втрачено, відновити цей акаунт стане неможливо.", title: "Двофакторна автентифікація", 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', " - Скинути при наступному вході."); }
                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', " - Скинути при наступному вході."); }
                    else if (seconds < 3600) { var secs = Math.floor(seconds / 60); QH('p2nextPasswordUpdateTime', format((secs == 1) ? " - Скинути за 1 хвилину." : " - Скинути за {0} хвилин(и).", secs)); }
                    else if (seconds < 86400) { var hours = Math.floor(seconds / 3600); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Скинути за 1 годину." : " - Скинути за {0} годин(и).", hours)); }
                    else { var days = Math.floor(seconds / 86400); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Скинути за 1 день." : " - Скинути за {0} дні(в).", 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 секунда до відключення" : "{0} секунд до відключення");
                } else {
                    ds = Math.round(ds / 60);
                    if (ds <= 5) {
                        theText = isLogout
                            ? (ds == 1 ? "1 minute until logout" : "{0} minutes until logout")
                            : (ds == 1 ? "1 хвилина до відключення" : "{0} хвилин до відключення");
                    }
                }
                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', "Немає");
                        }
                    }
                    break;
                }
                case 'serverstats': {
                    updateGeneralServerStats(message);
                    break;
                }
                case 'serverwarnings': {
                    if ((message.warnings != null) && (message.warnings.length > 0)) {
                        var ServerWarnings = {
                            1: "",
                            2: "Параметри WebDAV відсутні.",
                            3: "Нерозпізнаний параметр конфігурації \"{0}\".",
                            4: "Стиснення WebSocket вимкнено, ця властивість не працює в NodeJS v11.11 до v12.15 і v13.2",
                            5: "Не вдалося завантажити кореневий сертифікат Intel AMT TLS для домену за замовчуванням.",
                            6: "Не вдалося завантажити кореневий сертифікат Intel AMT TLS для домену {0}.",
                            7: "Локальні імена FQDN CIRA ігноруються, коли сервер працює в режимі лише LAN або лише WAN.",
                            8: "Не можна мати більше 4 локальних FQDN CIRA. Ігнорування значення.",
                            9: "Перевірку хешу агента проігноровано, це небезпечно.",
                            10: "Відсутня адреса е-пошти Let's Encrypt.",
                            11: "Хибні імена хостів Let's Encrypt.",
                            12: "Хибні імена Let's Encrypt, бо не можуть містити *.",
                            13: "Неможливо встановити модуль Let's Encrypt.",
                            14: "Хибні імена Let's Encrypt, неможливо уладнати: {0}",
                            15: "Хибна адреса е-пошти Let's Encrypt, неможливо уладнати: {0}",
                            16: "Неможливо завантажити список IPv6-адрес довіреного проксі CloudFlare.",
                            17: "Сервер SendGrid має обмежене використання в режимі LAN.",
                            18: "Сервер SMTP обмежено використовується в режимі локальної мережі.",
                            19: "Шлюз SMS має обмежене використання в режимі локальної мережі.",
                            20: "Хибний \"LoginCookieEncryptionKey\" у config.json.",
                            21: "Шлях резервного копіювання не можна встановити у теці meshcentral-data, параметри резервного копіювання ігноруються.",
                            22: "Помилка підпису агента {0}: {1}",
                            23: "Неможливо завантажити файл іконки агента: {0}.",
                            24: "Не вдалося завантажити файл логотипу агента: {0}.",
                            25: "Ця версія NodeJS не підтримує OpenID.",
                            26: "Поточна версія NodeJS не підтримує 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>' + "УВАГА:" + 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>' + "УВАГА:" + 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>' + "УВАГА:" + format("Сертифікат закінчується через {0} д.", days) + '</b></div>');
                            QV('serverWarningsDiv', true);
                            addNotification({ text: format("Сертифікат закінчується через {0} д.", 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>' + "ПОМИЛКА:" + JSON.parse(message.value).error + '</span>');
                            QH('p18html', '<div style="text-align:center;padding:50px;color:gray"><i>' + "Натисніть Оновити для завантаження встановленого ПЗ..." + '</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>' + "Нещодавно був:" + '<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>' + "Успіх" + '</span>' : '<span style=color:red>' + "Помилка" + '</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>' + "Назва Процесу" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.processName + '</b></div></div>'; }
                                        if (message.value.machineName) { x += addHtmlValue5("Ім'я Машини", message.value.machineName); }
                                        if (message.value.cmd) { x += '<div style=padding:4px><div style=padding-bottom:4px>' + "Командний рядок" + '</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>' + "Заголовок вікна" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.mainWindowTitle + '</b></div></div>'; }
                                        if (message.value.processUser) { x += addHtmlValue5("Користувач", message.value.processUser); }
                                        if (message.value.processDomain) { x += addHtmlValue5("Домен", message.value.processDomain); }
                                        if (message.value.startTime) { x += addHtmlValue5("Час запуску", message.value.startTime); }
                                        x += '<hr />';
                                        if (message.value.PriorityBoostEnabled) { x += addHtmlValue5("Підвищення пріоритету", message.value.PriorityBoostEnabled ? "Увімкнено" : "Вимкнено"); }
                                        if (message.value.sessionId) { x += addHtmlValue5("Ідентифікатор сеансу", message.value.sessionId); }
                                        if (message.value.handleCount) { x += addHtmlValue5("Кількість Обробок", message.value.handleCount); }
                                        if (message.value.privilegedProcessorTime) { x += addHtmlValue5("Привілейований процесорний час", format("{0} секунд", message.value.privilegedProcessorTime)); }
                                        if (message.value.totalProcessorTime) { x += addHtmlValue5("Загальний час процесора", format("{0} секунд", message.value.totalProcessorTime)); }
                                        if (message.value.userProcessorTime) { x += addHtmlValue5("Час процесора користувача", format("{0} секунд", message.value.userProcessorTime)); }
                                        if (message.value.nonpagedSystemMemorySize) { x += addHtmlValue5("Невивантажувана пам'ять", getNiceSize2(message.value.nonpagedSystemMemorySize)); }
                                        if (message.value.pagedMemorySize) { x += addHtmlValue5("Сторінкова Пам'ять", getNiceSize2(message.value.pagedMemorySize)); }
                                        if (message.value.privateMemorySize) { x += addHtmlValue5("Приватна пам'ять", getNiceSize2(message.value.privateMemorySize)); }
                                        if (message.value.virtualMemorySize) { x += addHtmlValue5("Віртуальна пам'ять", getNiceSize2(message.value.virtualMemorySize)); }
                                        if (message.value.workingSet) { x += addHtmlValue5("Робочий набір", getNiceSize2(message.value.workingSet)); }
                                        if (message.value.peakWorkingSet) { x += addHtmlValue5("Піковий робочий набір", getNiceSize2(message.value.peakWorkingSet)); }
                                        if (message.value.peakPagedMemorySize) { x += addHtmlValue5("Пік Сторінкової Пам'яті", getNiceSize2(message.value.peakPagedMemorySize)); }
                                        if (message.value.peakVirtualMemorySize) { x += addHtmlValue5("Пікова віртуальна пам'ять", getNiceSize2(message.value.peakVirtualMemorySize)); }
                                    } else {
                                        x += "Немає інформації";
                                    }
                                    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("Перше підключення агента", printDateTime(new Date(currentNode.firstconnect))); }
                        if (currentNode.lastconnect) { x += addHtmlValue2("Востаннє агент було підключено", printDateTime(new Date(currentNode.lastconnect))); }
                        if (currentNode.lastaddr) {
                            var splitip = currentNode.lastaddr.split(':');
                            if (splitip.length > 2) {
                                // IPv6
                                x += addHtmlValue2("Остання IP-адреса агента", currentNode.lastaddr + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(currentNode.lastaddr) + '")></i>');
                            } else {
                                // IPv4
                                if (isPrivateIP(currentNode.lastaddr)) {
                                    x += addHtmlValue2("Остання IP-адреса агента", splitip[0] + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                } else {
                                    x += addHtmlValue2("Остання IP-адреса агента", '<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="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                }
                            }
                        }
                        if (message.updateTime != null) { x += addHtmlValue2("Останні оновлення інтерфейсу", 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("Ім'я", '<b>' + EscapeHtml(net.name) + '</b>'); }
                                if (net.desc) { x += addHtmlValue2("Опис", EscapeHtml(net.desc).replace('(R)', '&reg;').replace('(r)', '&reg;')); }
                                if (net.dnssuffix) { x += addHtmlValue2("DNS суфікс", EscapeHtml(net.dnssuffix) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Скопіювати назву до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.dnssuffix) + '")></i>'); }
                                if (net.mac) { x += addHtmlValue2("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="' + "Копіювати MAC-адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.mac.toLowerCase()) + '")></i>'); }
                                if (net.v4addr) { x += addHtmlValue2("IPv4-адреса", EscapeHtml(net.v4addr) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4addr) + '")></i>'); }
                                if (net.v4mask) { x += addHtmlValue2("Маска IPv4", EscapeHtml(net.v4mask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4mask) + '")></i>'); }
                                if (net.v4gateway) { x += addHtmlValue2("Шлюз IPv4", EscapeHtml(net.v4gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4gateway) + '")></i>'); }
                                if (net.gatewaymac) { x += addHtmlValue2("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="' + "Копіювати MAC-адресу до буфера обміну" + '" 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("Ім'я", '<b>' + EscapeHtml(i) + '</b>');
                                if (typeof net[0].mac == 'string') { x += addHtmlValue2("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="' + "Копіювати MAC-адресу до буфера обміну" + '" 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("IPv6-адреса", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("Маска IPv6", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("Шлюз IPv6", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    } else if (netif.family == 'IPv4') {
                                        if (netif.address) { x += addHtmlValue2("IPv4-адреса", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("Маска IPv4", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("Шлюз IPv4", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Копіювати адресу до буфера обміну" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    }
                                }
                            }
                        } else {
                            x += '<hr />'
                            x += "Для цього пристрою немає інформації про мережевий інтерфейс.";
                        }
                        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 = "Невідомо"; }
                        if (!message.tags.stable) { message.tags.stable = "Невідомо"; }
                        if (!message.tags.latest) { message.tags.latest = "Невідомо"; }
                        x += addHtmlValue2("Поточна Версія", '<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 == "Невідомо") || (message.tags.current == message.tags.stable) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Стабільна версія" + '</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 == "Невідомо") || (message.tags.current == message.tags.latest) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Остання Версія" + '</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', "Версія MeshCentral", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        } else {
                            x += '<hr />' + "Виберіть і натисніть OK, щоб почати самооновлення сервера.";
                            xxdialogTag = message.tags;
                            setModalContent('xxAddAgent', "Версія 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', "Помилки Сервера", "Журнал помилок пустий або його не існує.");
                            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="' + "Завантажити журнал помилок" + '" onclick=d2CopyServerErrorsToClip()></i></div>';
                            x += '<div><label><input id=d2clearErrorsCheck type=checkbox class="form-check-input me-2" onclick=server_showErrorsDlgUpdate() /> ' + "Виберіть і натисніть OK, щоб очистити журнал помилок." + '</label></div>';
                            setModalContent('xxAddAgent', "Помилки сервера MeshCentral", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', server_showErrorsDlgEx);
                            server_showErrorsDlgUpdate();
                        }
                    }
                    break;
                }
                case 'serverconfig': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'MeshCentralServerConfig')) {
                        if (message.data == null) {
                            setModalContent('xxAddAgent', "Конфігурація Сервера", 'Server has no config file.', 'extra-large');
                        } else {
                            setModalContent('xxAddAgent', "Конфігурація Сервера", 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 = ['', "ДФА заблоковано", "Резервні коди заблоковано", "Токен входу використовується", "OTP 2FA заборонено", "Акаунт заблоковано", "Не вдалося завантажити OTPLIB"];
                            if ((message.err > 0) && (message.err < otpauthErrors.length)) { QH('d2optinfo', otpauthErrors[message.err]); } else { QH('d2optinfo', format("Помилка № {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("Інсталювати" + ' <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" rel="noreferrer noopener" target=_blank>' + "Google Authenticator" + '</a> ' + "або сумісну програму та відскануйте QR-код, скориставшись <a href=\"{0}\" rel=\"noreferrer noopener\" target=_blank>цим посилання</a> або введіть цей секретний код вручну. Потім введіть поточний 6-значний токен, щоб активувати двофакторний вхід.", message.url) + '<br /><br />' + 'Secret <img src=images/link4.png height=10 width=10 title="' + "Скопіюйте секретний ключ до буфера обміну" + '" 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 />' + "Введіть тут свій двофакторний токен входу:" + ' <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', "Застосунок Автентифікатора", message.success ? ('<b style=color:green>' + "Двофакторну автентифікацію успішно ввімкнено." + '</b> ' + "Тепер вам знадобиться чинний токен для нового підключення.") : ('<b style=color:red>' + "активація двофакторної автентифікації невдала." + '</b> ' + "Очистіть секретний ключ із програми та повторіть спробу. У вас є лише кілька хвилин, щоб ввести відповідний код."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-clear': {
                    if (xxdialogMode) return;
                    setModalContent('xxAddAgent', "Застосунок Автентифікатора", message.success ? ('<b>' + "Вилучено двофакторну автентифікацію." + '</b> ' + "Ви можете повторно активувати цю властивість в будь-який час.") : ('<b style=color:red>' + "видалення двофакторної автентифікації невдале." + '</b> ' + "Спробуйте знову."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-getpasswords': {
                    if (xxdialogMode == 2 && (xxdialogTag != 'otpauth-manage')) return;
                    var x = "Одноразові токени можна використовувати як додаткову автентифікацію. Згенеруйте набір, роздрукуйте їх і зберігайте в безпечному місці.";
                    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>' + "Немає Активних Токенів";
                    }
                    x += '</table></div></div><br />';
                    x += '<div class="btn-group">';
                    x += '<button type=button class="btn btn-warning" onclick="account_manageOtp(1);">' + "Згенерувати Нові Токени" + '</input>';
                    if (message.passwords != null) {
                        x += '<button type=button class="btn btn-danger" onclick="account_manageOtp(2);">' + "Очистити Токени" + '</button></div>';
                        x += '&nbsp;<i class="fa-regular fa-clipboard" title="' +  "Скопіювати дійсні коди до буфера обміну" + '" style="cursor:pointer" onclick=copyTextToClip2("' + encodeURIComponentEx(clipb) + '")></i>';
                    } else {
                        x += '</div>'
                    }
                    xxdialogTag = 'otpauth-manage';
                    setModalContent('xxAddAgent', "Керувати Резервними Кодами", 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">' + "Апаратні ключі" + '</a> ' + "використовуються для двофакторної автентифікації.";
                    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 + ')>' + "Видалити" + '</button>' + end;
                        }
                    } else {
                        x += start + '<tr style=text-align:center><td>' + "Ключі Не Налаштовано" + 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);">' + "Додати Ключ" + '</input>'; }
                        if ((features & 0x00004000) != 0) { x += '<button class="btn btn-primary" id=d2addkey2 type=button onclick="account_addhkey(2);">' + "Додати YubiKey&reg; OTP" +'</input>'; }
                    } else {
                        x += "Досягнуто Максимум Ключів.";
                    }
                    x += '</div><br />';
                    xxdialogTag = 'otpauth-hardware-manage';
                    setModalContent('xxAddAgent', "Керувати Ключами Безпеки", 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', "Додати Ключ Безпеки", '<br />' + "Помилка, Неможливо додати ключ." + '<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', "Додати Ключ Безпеки", '<br />' + "Помилка, Неможливо додати ключ." + '<br /><br />');
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');

                    }
                    break;
                }
                case 'webauthn-startregister': {
                    if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
                    var x = "Натисніть кнопку ключа зараз." + '<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', "Додати Ключ Безпеки", 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);
                        setModalContent('xxAddAgent', "Додати Ключ Безпеки", "ПОМИЛКА:" + 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>' + "Для перевірки телефону, введіть код підтвердження після отримання.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Код верифікації:" + ' <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', "Телефонні Сповіщення", 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>' + "Для перевірки програми обміну повідомленнями, введіть код підтвердження після отримання.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Код верифікації:" + ' <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', "Сповіщення месенджера", 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("Запит допомоги від {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 = "Агент підключено";
                                    if ((node.agent != null) && (node.agent.root === false)) { agentPrivilages = "Агент підключено із обмеженими привілеями"; }
                                    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 підключено"; addNotification(abc); }
                                    if (((node.conn & 4) == 0) && ((message.event.conn & 4) != 0)) { abc.text = "Intel AMT підключено"; addNotification(abc); }
                                    if (((node.conn & 16) == 0) && ((message.event.conn & 16) != 0)) { abc.text = "MQTT підключено"; 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 = "Агент відключений"; addNotification(abc); }
                                    if (((node.conn & 2) != 0) && ((message.event.conn & 2) == 0)) { abc.text = "Intel AMT CIRA припинено"; addNotification(abc); }
                                    if (((node.conn & 4) != 0) && ((message.event.conn & 4) == 0)) { abc.text = "Intel AMT припинено"; addNotification(abc); }
                                    if (((node.conn & 16) != 0) && ((message.event.conn & 16) == 0)) { abc.text = "MQTT відключено"; 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>' + "Неможливо просканувати цей діапазон адрес." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Приклад значення діапазону IP" + '<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 += " з TLS."; } else { str += " без 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>' + "Сканування не дало результатів." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Приклад значення діапазону IP" + '<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', "Немає");
                                }
                            }
                            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 не може надіслати повідомлення про подію:", 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("Дозволи успішно збережено");
                    } else {
                        alert("Помилка збереження дозволів: " + 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 година" : "{0} годин", message.expire);
                    if (message.expire == 24) { t = "1 день"; }
                    if (message.expire == 168) { t = "1 тиждень"; }
                    if (message.expire == 5040) { t = "1 місяць"; }
                    if (message.expire == 0) { t = "Необмежено"; }
                    QH('agentInvitationLink', format("Лінк Запрошення ({0})", t));
                    QV('agentInvitationLinkDiv', true);
                    break;
                }
                case 'createDeviceShareLink': { // Guest sharing link
                    var node = getNodeFromId(message.nodeid), x = '';
                    if (node == null) break;
                    x += addHtmlValue("Пристрій", EscapeHtml(node.name));
                    x += addHtmlValue("Ім'я Гостя", message.guestname);
                    x += addHtmlValue("Введення користувача", message.viewOnly ? "Заборонено, лише перегляд" : "Дозволено");
                    if (message.start && message.expire) {
                        if (message.recurring) {
                            x += addHtmlValue("Час запуску", printDateTime(new Date(message.start)));
                            if (message.expire < 2) { x += addHtmlValue("Тривалість", format("{0} хвилин", message.expire)); }
                            else { x += addHtmlValue("Тривалість", format("{0} хвилин", message.expire)); }
                        } else {
                            x += addHtmlValue("Час запуску", printDateTime(new Date(message.start)));
                            x += addHtmlValue("Термін придатності", printDateTime(new Date(message.expire)));
                        }
                    }
                    if (message.recurring == 1) { x += addHtmlValue("Повторити", "Щодня"); }
                    if (message.recurring == 2) { x += addHtmlValue("Повторити", "Щотижня"); }
                    var y = [];
                    if (message.consent & 0x0007) { y.push("Сповістити"); }
                    if (message.consent & 0x0038) { y.push("Запит"); }
                    if (message.consent & 0x0040) { y.push("Панель конфіденційності"); }
                    if (y.length == 0) { y.push("Немає"); }
                    x += addHtmlValue("Згода користувача", y.join(', '));
                    var type = ''; if (message.p <= 7) { type = ['', "Підключення до Віддаленого Терміналу ", "Підключення до Віддаленої Стільниці", "Віддалена стільниця + посилання на термінал", "Посилання на Віддалені Файли", "Віддалений Термінал + Посилання на Файл", "Підключення до віддаленої стільниці + файли", "Віддалена стільниця + термінал + посилання на файли"][message.p]; } else if (message.p == 8) { type = format("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="' + "Копіювати посилання до буфера обміну" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></i></div></div>';
                    setModalContent('xxAddAgent', "Надати доступ до пристрою", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getmqttlogin': {
                    if ((currentNode == null) || (currentNode._id != message.nodeid) || (xxdialogMode != null)) return;
                    var x = "Ці налаштування можна використовувати для підключення MQTT до цього пристрою." + '<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', "Облікові дані 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', "Дія Плаґіна", format('Select the version to downgrade the plugin: {0}', message.info.name) + '<hr />' + vSelect + '<hr />' + "Зверніть увагу, що не рекомендується повертатися до попередньої версії. Зробіть це лише у випадку, якщо нещодавнє оновлення щось зіпсувало." + + '<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>' + "Перейдіть за URL-адресою нижче, надайте доступ і скопіюйте код токена назад." + '</div><br />';
                            x += addHtmlValue("Активація", '<a href=\"' + message.url + '\" rel="noreferrer noopener" target="_blank">Browse to this URL</a>');
                            x += addHtmlFormFloating("Код", '<input id=gdcode onkeyup=server_setupGoogleDriveBackupCheck3() class=form-control></input>');
                            setModalContent('xxAddAgent', "Резервне копіювання до Google Drive", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_setupGoogleDriveBackupEx(3, 'gd2'));
                            Q('gdcode').focus();
                            QE('idx_dlgOkButton', false);
                        }
                    }
                    break;
                }
                case 'pluginError': {
                    setModalContent('xxAddAgent', "Помилка Плаґіна", 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 = "Успішний вхід"; c = 'BBD1BB'; xx = ''; if (message.events[i].tn != null) { m = format("Токен: {0}", message.events[i].tn); c = '88D188' } }
                            else if (m == 108) { m = "Хибний 2FA"; c = 'DD9DC3'; xx = 'x'; }
                            else if (m == 109) { m = "Заблокований акаунт"; c = 'E1BBBB'; xx = 'x'; }
                            else if (m == 110) { m = "Хибний пароль"; 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', "Попередні спроби входу", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getDeviceDetails': {
                    saveAs(new Blob([message.data], { type: 'application/octet-stream' }), "перелік пристроїв" + '.' + message.type);
                    break;
                }
                case 'createLoginToken': { // A new login token was created
                    if (xxdialogMode) return;
                    var x = "Запам'ятайте це ім'я користувача та пароль, бо не можна знову відобразити пароль." + '<br /><br />';
                    x += addHtmlValue("Ім'я", EscapeHtml(message.name))
                    if (message.expire != 0) { x += addHtmlValue("Дійсний до", EscapeHtml(printDateTime(new Date(message.expire)))); }
                    x += addHtmlValue("Ім'я користувача", '<i class="fa-regular fa-copy" title="' + "Копіювати посилання до буфера обміну" + '" 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("Пароль", '<i class="fa-regular fa-copy" title="' + "Копіювати посилання до буфера обміну" + '" 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 = "Закрити";
                    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' : '') + '>' + "Тип Агента" + '</label><br />';
            x += '<label><input id=d2c9 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('agver') >= 0) ? ' checked' : '') + '>' + "Версія Агента" + '</label><br />';
            x += '<label><input id=d2c6 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('desc') >= 0) ? ' checked' : '') + '>' + "Опис Пристрою" + '</label><br />';
            x += '<label><input id=d2c5 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('os') >= 0) ? ' checked' : '') + '>' + "Операційна система" + '</label><br />';
            x += '<label><input id=d2c10 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('tags') >= 0) ? ' checked' : '') + '>' + "Теги" + '</label><br />';
            x += '<label><input id=d2c11 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsav') >= 0) ? ' checked' : '') + '>' + "Windows AV" + '</label><br />';
            x += '<label><input id=d2c12 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) ? ' checked' : '') + '>' + "Windows Update" + '</label><br />';
            x += '<label><input id=d2c13 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) ? ' checked' : '') + '>' + "Фаєрвол Windows" + '</label><br />';
            x += '<label><input id=d2c14 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) ? ' checked' : '') + '>' + "Час останнього запуску" + '</label><br />';
            x += '<label><input id=d2c16 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('idletime') >= 0)?' checked':'') + '>' + "Час бездіяльності" + '</label><br />';
            x += '<label><input id=d2c1 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('links') >= 0) ? ' checked' : '') + '>' + "Посилання на MeshCentral Router" + '</label><br />';
            x += '<label><input id=d2c2 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('user') >= 0) ? ' checked' : '') + '>' + "Зареєстровані користувачі" + '</label><br />';
            x += '<label><input id=d2c3 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('ip') >= 0) ? ' checked' : '') + '>' + "IP-адреса Агента" + '</label><br />';
            x += '<label><input id=d2c4 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('conn') >= 0) ? ' checked' : '') + '>' + "Сполучення до Сервера" + '</label><br />';
            x += '<label><input id=d2c15 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amthost') >= 0) ? ' checked' : '') + '>' + "Ім'я хоста Intel&reg; AMT" + '</label><br />';
            x += '<label><input id=d2c17 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amtstate') >= 0) ? ' checked' : '') + '>' + "Стан Intel&reg; AMT" + '</label><br />';
            x += '<label><input id=d2c7 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastseen') >= 0) ? ' checked' : '') + '>' + "Востаннє підключено" + '</label><br />';
            setModalContent('xxAddAgent', "Стовпці Перегляду Пристрою", 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>' + ", лише Intel&reg; AMT" + '</span>'; }
                            if (meshes[node.meshid] && (meshes[node.meshid].mtype == 3)) { if (meshes[node.meshid].relayid) { extra = '<span class=devHeaderx>' + ", Пов'язані Пристрої" + '</span>'; } else { extra = '<span class=devHeaderx>' + ", Локальні Пристрої" + '</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>' + "Окремі Пристрої" + '</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>' + "Окремі Пристрої" + '</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>' + "Окремі Пристрої" + '</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">' + "Жоден пристрій не включено до жодної групи, клікніть \"Групи\" пристроїв, щоб додати їх." + '</div>';
                    } else {
                        r = '<div style="margin:30px">' + "Немає пристроїв, які відповідають цьому запиту." + ' <a onclick=clearDeviceSearch()>' + "Очистити фільтр пошуку" + '</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>' + "Немає пристроїв Intel&reg; AMT у цій групі пристроїв";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "додайте перший" + '</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>' + "У цій групі ще нема пристроїв";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "додайте перший" + '</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>' + "Немає локальних пристроїв у цій групі пристроїв";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "додайте перший" + '</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>' + "У цій групі ще нема пристроїв";
                            }
                            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>' + "Тип Агента"; }
                        if (deviceViewSettings.devsCols.indexOf('agver') >= 0) { colums += '<th style=width:100px>' + "Версія Агента"; }
                        if (deviceViewSettings.devsCols.indexOf('os') >= 0) { colums += '<th style=width:160px>' + "ОС"; }
                        if (deviceViewSettings.devsCols.indexOf('tags') >= 0) { colums += '<th style=width:120px>' + "Теги"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsav') >= 0) { colums += '<th style=width:100px>' + "Windows AV"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) { colums += '<th style=width:120px>' + "Windows Update"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) { colums += '<th style=width:120px>' + "Фаєрвол Windows"; }
                        if (deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) { colums += '<th style=width:120px>' + "Час останнього запуску"; }
                        if (deviceViewSettings.devsCols.indexOf('idletime') >= 0) { colums += '<th style=width:100px title="' + "Оновлено кожні 5 хвилин" + '">' + "Час бездіяльності"; }
                        if (deviceViewSettings.devsCols.indexOf('links') >= 0) { colums += '<th style=width:120px>' + "Лінк"; }
                        if(features3 & 0x00000002){
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:180px>' + "Користувач"; }
                        } else {
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:120px>' + "Користувач"; }
                        }
                        if (deviceViewSettings.devsCols.indexOf('ip') >= 0) { colums += '<th style=width:120px>' + "Адреса"; }
                        if (deviceViewSettings.devsCols.indexOf('conn') >= 0) { colums += '<th style=width:100px>' + "З'єднання"; }
                        if (deviceViewSettings.devsCols.indexOf('amthost') >= 0) { colums += '<th style=width:100px>' + "AMT Host"; }
                        if (deviceViewSettings.devsCols.indexOf('amtstate') >= 0) { colums += '<th style=width:100px>' + "AMT State"; }
                        if (deviceViewSettings.devsCols.indexOf('lastseen') >= 0) {
                            colums += '<th style=width:120px>' + "Востаннє підключено";
                            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>' + "Пристроїв із тегами не знайдено." + '</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="' + "Створити нову групу пристроїв." + '" style=cursor:pointer>' + "Додати Групу Пристроїв" + '</a>&nbsp';
                }
                if ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 128) == 0)) {
                    r += '<a href=# onclick=\'return p10showMeshCmdDialog(0)\' style=cursor:pointer title="' + "Завантажте MeshCmd, засіб командного рядка, що виконує багато функцій." + '">' + "MeshCmd" + '</a>&nbsp';
                    if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) { r += '<a href=# onclick=\'return p10showMeshRouterDialog()\' style=cursor:pointer title="' + "Завантажити MeshCentral Router, засіб пересилання портів TCP." + '">' + "Router" + '</a>&nbsp'; }
                }
                r += '</div><div style=height:60px></div>';

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

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

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

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

                    for (var i in multiDesktop) { multiDesktop[i].xxdelete = true; }
                    for (var i in kvmDivs) {
                        var id = kvmDivs[i], shortid = id.split('/')[2], desk = multiDesktop[id];
                        if (desk != null) {
                            // This device already has a canvas, use it.
                            var width = (view == 5) ? ((desk.m.width * vsize.y) / desk.m.height) : vsize.x;
                            desk.m.CanvasId.setAttribute('style', 'background-color:black;width:' + width + 'px;height:' + vsize.y + 'px');
                            Q('xkvmid_' + shortid).appendChild(desk.m.CanvasId);
                            delete desk.xxdelete;
                            QH('skvmid_' + shortid, ["Відключено", "Підключення...", "Установити...", '', ''][((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, ["Відключено", "Підключення...", "Установити...", '', ''][((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>' + "Немає" + '</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>' + "Немає" + '</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 = "Пристрій підключено"; }
                    if (bat.state == 'dc') { statestr = "Пристрій живиться від батареї"; }

                    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="' + "Порт комутатора готовий до використання." + '">' + "Перемикач" + '</span>');
                            } else {
                                states.push('<span title="' + "Порт IP-KVM підключено та готовий до використання." + '">' + "IP-KVM" + '</span>');
                            }
                        } else {
                            states.push('<span title="' + "MeshAgent підключений та готовий до використання" + '">' + "Агент" + '</span>');
                        }
                    }
                    if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA підключено та готово до використання." + '">' + "CIRA" + '</span>'); }
                    else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT маршрутизується." + '">' + "AMT" + '</span>'); }
                    if ((node.conn & 8) != 0) { states.push('<span title="' + "MeshAgent доступний із використанням іншого агента ретрансляції." + '">' + "Ретрансляція" + '</span>'); }
                    if ((node.conn & 16) != 0) { states.push('<span title="' + "З'єднання MQTT із пристроєм активне." + '">' + "MQTT" + '</span>'); }
                }
                if (node.mtype == 3) {
                    var mesh = meshes[node.meshid];
                    if (mesh && mesh.relayid) {
                        states.push('<span title="' + "Підключення до локальної мережі через агент ретрансляції." + '">' + "Ретрансляція" + '</span>');
                    } else {
                        states.push('<span title="' + "Підключення до локальної мережі." + '">' + "Локальна" + '</span>');
                    }
                }

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

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

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

                // Display configured columns
                if (deviceViewSettings.devsCols.indexOf('agtype') >= 0) { r += '<td style=text-align:center;font-size:x-small;padding-left:4px;padding-right:4px title="' + EscapeHtml(((node.mtype != 3) && node.agent && node.agent.id && (node.agent.id <= agentsStr.length)) ? agentsStr[node.agent.id] : '') + '">' + EscapeHtml(((node.mtype != 3) && node.agent && node.agent.id && (node.agent.id <= agentsStr.length)) ? agentsStr[node.agent.id] : '').replace(',', '<br />'); } // Agent type
                if (deviceViewSettings.devsCols.indexOf('agver') >= 0) { r += '<td style=text-align:center;font-size:x-small;padding-left:4px;padding-right:4px title="' + EscapeHtml((node.agent && node.agent.core) ? node.agent.core : '') + '">' + EscapeHtml((node.agent && node.agent.core) ? node.agent.core : '').replace(',', '<br />'); } // Agent core
                if (deviceViewSettings.devsCols.indexOf('os') >= 0) { // Operating System
                    if (node.mtype == 3) {
                        var osstr = ''; if (node.agent.id == 4) { osstr = 'Windows'; } if (node.agent.id == 6) { osstr = 'Linux'; } if (node.agent.id == 29) { osstr = 'MacOS'; }
                        r += '<td style=text-align:center;font-size:x-small title="' + EscapeHtml(osstr) + '">' + EscapeHtml(osstr);
                    } else {
                        r += '<td style=text-align:center;font-size:x-small title="' + EscapeHtml(node.osdesc ? node.osdesc : '') + '">' + EscapeHtml(node.osdesc ? node.osdesc : '');
                    }
                }
                if (deviceViewSettings.devsCols.indexOf('tags') >= 0) { // Tags
                    r += '<td style=text-align:center;font-size:x-small>';
                    var groupingTags = '';
                    if (node.tags != null) {
                        for (var i in node.tags) {
                            groupingTags += '<span class=tagSpan>' + EscapeHtml(node.tags[i]) + '</span> ';
                        }
                    }
                    r += '<span style=line-height:20px>' + groupingTags + '</span>';
                }
                if (deviceViewSettings.devsCols.indexOf('windowsav') >= 0) { // Windows AV
                    r += '<td style=text-align:center>' + ((node.wsc && node.wsc.antiVirus != null) ? (node.wsc.antiVirus == 'OK' ? '<span style=color:green>' + "OK" + '</span>' : '<span style=color:red>' + "КЕПСЬКО" + '</span>') : "");
                }
                if (deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) {// Windows Update
                    r += '<td style=text-align:center>' + ((node.wsc && node.wsc.autoUpdate != null) ? (node.wsc.autoUpdate == 'OK' ? '<span style=color:green>' + "OK" + '</span>' : '<span style=color:red>' + "КЕПСЬКО" + '</span>') : "");
                }
                if (deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) { // Windows Firewall
                    r += '<td style=text-align:center>' + ((node.wsc && node.wsc.firewall != null) ? (node.wsc.firewall == 'OK' ? '<span style=color:green>' + "OK" + '</span>' : '<span style=color:red>' + "КЕПСЬКО" + '</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 не активовано" + '">' + "Pre" + '</span>'; }
                        if (node.intelamt.state == 1) { amtstate = ' <span title="' + "Intel&reg; AMT знаходиться в режимі активації" + '">' + "Вх" + '</span>'; }
                        if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { amtstate = ' <span title="' + "Intel&reg; AMT активовано в Режимі Керування Клієнтом" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { amtstate += ' <span title="' + "Intel&reg; AMT активовано в Режимі Адміністратора" + '">' + "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 += "Підключено"; } 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 + '\')">' + "Відключено" + '</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) + '")>' + "Відхилити" + '</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', "Запити Допомоги" + ' - ' + 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>' + "Віддалена Стільниця" + '</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="' + "Відключити цю сесію" + '" class="fa-fw fa-solid fa-trash text-danger" ></i>'; }
                            x += addHtmlValue2(getUserName(j), ((node.sessions.kvm[j] == 1) ? "1 сесія" : nobreak(format("{0} сеанс", node.sessions.kvm[j]))) + trash);
                        } else if (j == 'busy') {
                            x += addHtmlValue2("Пристрій зайнятий", ((node.sessions.kvm[j] == 1) ? "1 сесія" : nobreak(format("{0} сеанс", node.sessions.kvm[j]))));
                        }
                    }
                } else if (i == 'multidesk') {
                    x += '<u>' + "Віддалена Стільниця" + '</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="' + "Відключити цю сесію" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.multidesk[j] == 1) ? "1 сесія" : nobreak(format("{0} сеанс", node.sessions.multidesk[j]))) + trash);
                    }
                } else if (i == 'terminal') {
                    x += '<u>' + "Термінал" + '</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="' + "Відключити цю сесію" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.terminal[j] == 1) ? "1 сесія" : nobreak(format("{0} сеанс", node.sessions.terminal[j]))) + trash);
                    }
                } else if (i == 'files') {
                    x += '<u>' + "Файли" + '</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="' + "Відключити цю сесію" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.files[j] == 1) ? "1 сесія" : nobreak(format("{0} сеанс", node.sessions.files[j]))) + trash);
                    }
                } else if (i == 'tcp') {
                    x += '<u>' + "Маршрутизація 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="' + "Відключити цю сесію" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.tcp[j] == 1) ? "1 сесія" : nobreak(format("{0} сеанс", node.sessions.tcp[j]))) + trash);
                    }
                } else if (i == 'udp') {
                    x += '<u>' + "Маршрутизація 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="' + "Відключити цю сесію" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.udp[j] == 1) ? "1 сесія" : nobreak(format("{0} сеанс", node.sessions.udp[j]))) + trash);
                    }
                }
            }
            if (x != '') {
                xxdialogTag = 'SESSIONS-' + node._id;
                setModalContent('xxAddAgent', "Сеанси" + ' - ' + 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', "Повідомлення Агента" + ' - ' + 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} користувачів", 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) + ' (' + "Заблоковано" + ')', (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', "Підключити Все", format("Ви впевнені, що хочете підключитися до {0} пристроїв?", 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, ["Відключено", "Підключення...", "Установити...", '', ''][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', "Налаштування Віддаленої Стільниці", 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="' + "Додати новий комп'ютер Intel&reg; AMT, що знаходиться у локальній мережі." + '" onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "Додати Локально" + '</a>';
                    r += ' <a href=# role="button" title="' + "Додати новий комп'ютер Intel&reg; AMT, просканувавши локальну мережу." + '" onclick=\'return addAmtScanToMesh("' + mesh._id + '")\'>' + "Сканувати Мережу" + '</a>';
                }
                if (mesh.amt && (mesh.amt.type > 0)) { // CCM Deactivate, CCM or ACM activation, Full Automatic
                    r += ' <a href=# role="button" title="' + "Виконати активацію та налаштування Intel&reg; AMT." + '" onclick=\'return showAmtSetup("' + mesh._id + '")\'>' + "Налаштувати" + '</a>';
                }
            }
            if ((mesh.mtype == 2) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { // Agent device group
                r += ' <a href=# role="button" title="' + "Додати новий комп'ютер до цієї групи пристроїв, встановивши MeshAgent." + '" onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "Додати агент" + '</a>';
                if ((features & 2) == 0) { r += ' <a href=# role="button" title="' + "Запросіть когось інсталювати MeshAgent у цю групу пристроїв." + '" onclick=\'return inviteAgentToMesh("' + mesh._id + '")\'>' + "Запросити" + '</a>'; }
            }
            if (mesh.mtype == 3) { // Local device group
                r += ' <a href=# role="button" title="' + "Додати пристрій, що знаходиться в локальній мережі." + '" onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "Додати Пристрій" + '</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("Додати локальний пристрій до групи пристроїв \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Назва Пристрою", '<input id=dp1devicename class=form-control maxlength=32 autocomplete=off onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Ім'я хоста", '<input id=dp1hostname class=form-control maxlength=32 autocomplete=off placeholder="' + "Те саме, що ім'я пристрою" + '" onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Тип", '<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', "Додати локальний пристрій", 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("Додати новий пристрій Intel&reg; AMT до групи пристроїв \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Назва Пристрою", '<input id=dp1devicename class="form-control" maxlength=32 autocomplete=off onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Ім'я хоста", '<input id=dp1hostname class="form-control" maxlength=32 autocomplete=off placeholder="' + "Те саме, що ім'я пристрою" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Ім'я користувача", '<input id=dp1username class="form-control" maxlength=32 autocomplete=off placeholder="' + "адміністратор" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Пароль", '<input id=dp1password type=password class="form-control" autocomplete=off maxlength=32 onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Безпека", '<select id=dp1tls class="form-select"><option value=0>' + "Немає безпеки TLS" + '</option><option value=1>' + "Необхідна безпека TLS" + '</option></select>');

            setModalContent('xxAddAgent', "Додати пристрій Intel® 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 = "Створіть кілька пристроїв Intel&reg; AMT одночасно, імпортувавши файл JSON у такому форматі:" + '<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', "Імпорт Пристроїв 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', "Імпорт Пристроїв Intel&reg; AMT", format("Хибний файл JSON: {0}.", ex));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    return;
                }
                if (j != null) {
                    meshserver.send({ action: 'importamtdevices', meshid: meshid, amtdevices: j });
                } else {
                    setModalContent('xxAddAgent', "Імпорт Пристроїв Intel&reg; AMT", "Хибний формат файлу JSON.");
                    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("Додати, активувати та налаштувати Intel&reg; AMT до групи \"{0}\", періодично запускаючи MeshCmd від імені адміністратора на віддаленому пристрої.", 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>' + "Для активації ACM для Intel&reg; AMT потрібно встановити наступне довірене FQDN:" + ' <b>' + serverinfo.amtAcmFqdn.join(', ') + '</b></div>');
            }
            setModalContent('xxAddAgent', "Налаштування 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>' + "Активуйте Intel® AMT у режимі адміністратора (Admin Control Mode = ACM) за допомогою USB-накопичувача у форматі FAT. Помістіть на нього файл setup.bin і завантажте один або кілька комп’ютерів за допомогою цього накопичувача." + '</div><div style=margin-top:6px>' + "Почніть із введення старого та нового паролів MEBx." + '</div></table>';
            x += addHtmlFormFloating("Старий Пароль", '<input id=dp1password0 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Новий Пароль*", '<input id=dp1password1 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Новий Пароль*", '<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" /> ' + "Використовується для bare-metal LAN активації." + '</label>'; } // Intel AMT LAN provisioning server is active.
            x += '<div><span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 символів, 1 велика літера, 1 маленька літера, 1 цифра, 1 спеціальний знак." + '</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 = "Введіть діапазон IP-адрес для пошуку пристроїв 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("Діапазон 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="' + "Сканувати" + '" onclick=addAmtScanToMeshButton()></input></tr></table>');
            } else {
                x += addHtmlValue("Діапазон 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="' + "Сканувати" + '" 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="' + "Скасувати" + '" style="" onclick="dialogclose(0)" /><input id="idx_dlgOkButton2" type="button" value="OK" style="" onclick="dialogclose(1)" /><div><input id="d2scanSelectButton" type="button" value="' + "Обрати Все" + '" onclick="scanSelectAll()" /></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Сканувати пристрої 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>' + "Приклад значення діапазону IP" + '<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>' + "Сканування..." + '</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("Тип Запрошення", '<select id=d2InviteType onchange=d2ChangedInviteType() class=form-select><option value=0>' + "Запрошення за посиланням" + '</option><option value=1>' + "Запрошення е-поштою" + '</option></select>') + '<hr />';
                x += '<div id=emailInviteDiv style=display:none>' + format("Запросіть встановити MeshAgent. Буде надіслана е-пошта із посиланням для інсталяції MeshAgent до групи пристроїв \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
                x += addHtmlFormFloating("Ім'я (необов'язково)", '<input id=agentInviteName value="" placeholder="Name (optional)" class="form-control" maxlength=64 />');
                x += addHtmlFormFloating("е-пошта", '<input id=agentInviteEmail placeholder="' + "приклад@email.ua" + '" class="form-control" onkeyup=validateAgentInvite() />');
                x += addHtmlFormFloating("Операційна система", '<select id=agentInviteNameOs onchange=d2ChangedInviteType() class=form-select><option value=4>' + "Надіслати лінк для інсталювання" + '</option><option value=0 selected>' + "Усі Можливі" + '</option><option value=1>' + "Лише Windows" + '</option><option value=3>' + "Лише Apple macOS" + '</option><option value=2>' + "Лише Linux" + '</option><option value=5>' + "MeshCentral Асистент" + '</option></select>');
                x += '<div id=d2agentexpirediv>';
                x += addHtmlFormFloating("Термін дії лінку", '<select id=agentInviteExpire class=form-select><option value=1>' + "1 година" + '</option><option value=8>' + "8 годин" + '</option><option value=24>' + "1 день" + '</option><option value=168>' + "1 тиждень" + '</option><option value=5040>' + "1 місяць" + '</option><option value=0>' + "Необмежено" + '</option></select>');
                x += '</div>';
                x += '<div id=d2agentInstallTypeDiv2>';
                x += addHtmlFormFloating("Тип Інсталяції", '<select id=agentInviteType class=form-select><option value=0>' + "Фоновий та Інтерактивний" + '</option><option value=2>' + "Лише фоновий" + '</option><option value=1>' + "Лише інтерактивний" + '</option></select>');
                x += '</div>';
                x += addHtmlFormFloating("Повідомлення" + ' ' + "(довільне)", '<textarea id="agentInviteMessage" class="form-control" style="height:100px;resize:none;" maxlength="1024"></textarea>');
                x += '</div>';
            }
            x += '<div id=urlInviteDiv>' + format("Запросіть когось встановити MeshAgent, поділившись посиланням-запрошенням. Цей лінк відкриє інструкцію встановлення програми для групи пристроїв \"{0}\". Покликання є загальнодоступним і не потребує акаунту на цьому сервері.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Термін дії лінку", '<select id=d2inviteExpire class=form-select onchange=d2RequestInvitationLink()><option value=1>' + "1 година" + '</option><option value=8>' + "8 годин" + '</option><option value=24>' + "1 день" + '</option><option value=168>' + "1 тиждень" + '</option><option value=5040>' + "1 місяць" + '</option><option value=0>' + "Необмежено" + '</option></select>');
            x += addHtmlFormFloating("Агент", '<select id=d2agentType class=form-select onchange=d2ChangedInviteType()><option value=0>' + "Усі доступні агенти" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "MeshCentral Асистент" + '</option><option value=16>' + "Android MeshAgent" + '</option></select>');
            x += '<div id=d2agentInstallTypeDiv>';
            x += addHtmlFormFloating("Тип Інсталяції", '<select id=d2agentInviteType class=form-select onchange=d2RequestInvitationLink()><option value=0>' + "Фоновий та Інтерактивний" + '</option><option value=2>' + "Лише фоновий" + '</option><option value=1>' + "Лише інтерактивний" + '</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="' + "Копіювати посилання до буфера обміну" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Запросити", 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>' + "Бінарний Інсталятор Linux / BSD / macOS" + '</option><option value=2>' + "Apple macOS" + '</option>';
            if ((features & 2) == 0) { opts += '<option value=6>' + "Мобільний пристрій" + '</option>'; } // Don't display mobile setup in LAN mode.
            opts += '<option value=7>' + "MeshCentral Асистент" + '</option>';
            opts += '<option value=3>' + "Windows (Деінсталяція)" + '</option><option value=4>' + "Linux / BSD (Деінсталяція)" + '</option><option value=8>' + "Apple macOS (Деінсталяція)" + '</option></select>';
            x += addHtmlFormFloating("Операційна система", 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("Тип системи", '<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("Тип Інсталяції", '<select id="aginsType" onchange="addAgentToMeshClick()" class="form-select"><option value="0">' + "Фоновий та Інтерактивний" + '</option><option value="2">' + "Лише фоновий" + '</option><option value="1">' + "Лише інтерактивний" + '</option></select>');
            x += '</div><div id="asinsTypeDiv">';
            x += addHtmlFormFloating("Тип Інсталяції", '<select id="asinsType" onchange="addAgentToMeshClick()" class="form-select"><option value="2">' + "Застосунок, Підключення за запитом користувача" + '</option><option value="3">' + "Застосунок, Завжди підключено" + '</option><option value="0">' + "Системний трей, підключення за запитом користувача" + '</option><option value="1">' + "Системний трей, завжди підключений" + '</option><option value="4">' + "Системний трей, лише монітор" + '</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("Щоб додати новий комп'ютер до групи пристроїв \"{0}\", завантажте MeshAgent й інсталюйте його на комп'ютері, яким хочете керувати. Завантажений агент міститиме у собі інформацію про цільовий сервер та групу пристроїв.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline id=aginsw32lnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "MeshAgent версія x86 32-бітна" + '">' + "Windows x86 32-біт (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Скопіюйте URL-адресу Windows x86 32-біт агента до буфера обміну" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline id=aginsw64lnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "MeshAgent версія x86_64 64-бітна" + '">' + "Windows x86-64 64-біт (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Скопіюйте URL-адресу Windows x86-64 64-біт агента до буфера обміну" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline id=aginswa64lnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "MeshAgent версія ARM 64-біт " + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати адресу до буфера обміну" + '" 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("Файл налаштувань", '<a id=aginswmshlnk name="meshsettings?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + format("{0} налаштування (.msh)", EscapeHtml(mesh.name)) + '</a>'); }
            x += '</div>';

            // Linux agent install
            x += '<div id=agins_linux style=display:none>' + format("Щоб додати комп'ютер до \"{0}\", виконайте таку команду. Будуть потрібні облікові дані root.", EscapeHtml(mesh.name)) + '<br />';
            x += '<textarea id=agins_linux_area rows=2 cols=20 readonly=readonly style=border-radius:4px;padding:6px;margin-top:4px;margin-bottom:4px;border:0;width:100%;resize:none;height:160px;overflow:auto;font-size:12px class=form-control></textarea>';
            x += '<div style=font-size:x-small;float:right>' + "* Для BSD, запустіть \"pkg install wget sudo bash\" спочатку." + '</div><a style=text-decoration:none title="' + "Копіювати до буфера обміну" + '" onclick=copyAgentIdValue("agins_linux_area")>' + "Копіювати" + ' <i class="fa-regular fa-clipboard" style=cursor:pointer></i></a></div>';

            // macOS agent install
            x += '<div id=agins_osx style=display:none>' + format("Щоб додати новий комп'ютер до групи пристроїв \"{0}\", завантажте MeshAgent й інсталюйте його на комп'ютері, яким ви хочете керувати. Цей інсталяційний агент містить інформацію про сервер та групу пристроїв.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Mesh Agent", '<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-64" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати URL-адресу macOS агента до буфера обміну" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=16&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agent", '<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-біт)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати URL-адресу macOS агента до буфера обміну" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=29&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Універсальна версія macOS MeshAgent" + '">' + "macOS (Universal)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати URL-адресу macOS агента до буфера обміну" + '" 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("Щоб додати мобільний пристрій до групи \"{0}\", завантажте програму MeshAgent і відскануйте цей QR-код.", EscapeHtml(mesh.name)) + '</div>';
            x += '<table style=width:180px>';
            x += '<tr><td style=text-align:center><a title="' + "Крамниця Google Play" + '"rel="noreferrer noopener" target=_blank href="https://play.google.com/store/apps/details?id=com.meshcentral.agent2"><img style=cursor:pointer src="images/google-play-140.png" width=140 srcset="images/google-play-280.png 2x" /></a></td></tr>';
            x += '<tr><td style=text-align:center><a title="' + "Amazon App Store" + '" rel="noreferrer noopener" target=_blank href="https://www.amazon.co.uk/gp/product/B097Z4Q7SK/"><img style=cursor:pointer src="images/amazon-appstore-140.png"  width=140 srcset="images/amazon-appstore-280.png 2x" /></a></td></tr>';
            x += '</table>';
            x += addHtmlValue("Android APK", '<a class=text-decoration-underline onclick=downloadFile("meshagents?id=14' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '",null,true) title="' + "MeshAgent версія APK" + '">' + "APK" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати URL до буфера обміну" + '" 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 — це інструмент Windows, за допомогою якого користувачі можуть отримати допомогу. Скористайтеся посиланням нижче, щоб завантажити версію, яка підключатиметься до групи пристроїв \"{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 для Windows" + '">' + "Помічник Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати URL до буфера обміну" + '" 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 — це інструмент Windows, за допомогою якого користувачі можуть отримати допомогу. Скористайтеся посиланням нижче, щоб завантажити версію, яка контролюватиме агента у фоновому режимі." + '</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 для Windows" + '">' + "Помічник Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати URL до буфера обміну" + '" 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>' + "Щоб видалити MeshAgent, завантажте файл нижче, запустіть його та натисніть \"видалити\"." + '<br /><br />';
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline id=aginsw32unlnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "MeshAgent версія x86 32-бітна" + '">' + "Windows x86 32-біт (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Скопіюйте URL-адресу Windows x86 32-біт агента до буфера обміну" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline id=aginsw64unlnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "MeshAgent версія x86_64 64-бітна" + '">' + "Windows x86-64 64-біт (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Скопіюйте URL-адресу Windows x86-64 64-біт агента до буфера обміну" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline id=aginswa64unlnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "MeshAgent версія ARM 64-біт " + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати адресу до буфера обміну" + '" 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>' + "Щоб видалити MeshAgent, виконайте наступну команду. Будуть потрібні облікові дані користувача root." + '<br />';
            x += '<textarea id=agins_linux_area_un rows=2 cols=20 readonly=readonly style=border-radius:4px;padding:6px;margin-top:4px;margin-bottom:4px;border:0;width:100%;resize:none;height:160px;overflow:auto;font-size:12px  class=form-control></textarea>';
            x += '<a style=text-decoration:none title="' + "Копіювати до буфера обміну" + '" 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>' + "Щоб видалити MeshAgent, завантажте файл нижче, розпакуйте ZIP-архів, потім клацніть правою кнопкою миші на \"Uninstall.command\" і виберіть \"Відкрити\"." + '<br /><br />';
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Універсальна версія macOS MeshAgent" + '">macOS Agent (Universal)</a> <i class="fa-regular fa-clipboard" title="' + "Копіювати URL-адресу macOS агента до буфера обміну" + '" 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>' + "Це виконуваний файл для ОС із графічним інтерфейсом." + '<br /><br />' + "Для запуску виконувані файли Apple macOS потрібно буде вилучити з карантину запустивши 'xattr -r -d com.apple.quarantine meshagent'." + '<br /><br />' + "Потрібно виконати 'chmod +x meshagent', та потім запустити цей файл." + '<br /><br />';
            x += addHtmlValue("Mesh Agent", '<a class=text-decoration-underline id=aginsbinlnk name="meshagents?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + "MeshAgent" + '</a> <i class="fa-regular fa-clipboard" title="' + "Скопіювати URL-адресу агента до буфера обміну" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Команда" + '&nbsp;<i class="fa-regular fa-clipboard" title="' + "Скопіювати URL-адресу агента до буфера обміну" + '" 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', "Додати Mesh Agent", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');

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

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

        var xxModal;

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

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

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

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

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

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

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

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

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

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

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

        function deviceHeaderSet() {
            if (deviceHeaderId == 0) { deviceHeaderId = 1; return; }
            deviceHeaders['DevxHeader' + deviceHeaderId] = ((deviceHeaderTotal == 1) ? "1 вузол" : format("{0} вузлів", 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="' + "Пристрій увімкнено." + '">' + "Увімкнено" + '</span>', '<span title="' + "Пристрій перебуває в стані сну (S1)." + '">' + "Засинання" + '</span>', '<span title="' + "Пристрій перебуває в стані сну (S2)." + '">' + "Засинання" + '</span>', '<span title="' + "Пристрій перебуває в стані глибокого сну (S3)." + '">' + "Режим сну" + '</span>', '<span title="' + "Пристрій перебуває в режимі глибокого сну (S4)." + '">' + "Гібернація" + '</span>', '<span title="' + "Пристрій у вимкненому стані (S5)." + '">' + "Soft-Off" + '</span>', '<span title="' + "Пристрій виявлено, але не вдалося отримати стан живлення." + '">' + "Поточний" + '</span>', '<span title="' + "Пристрій вимкнено." + '">' + "Вимк" + '</span>'];
        var powerStateStrings2 = ['', "Пристрій увімкнено", "Пристрій перебуває в стані сну (S1)", "Пристрій перебуває в стані сну (S2)", "Пристрій перебуває в глибокому сні (S3)", "Пристрій перебуває в гібернації (S4)", "Пристрій в стані м'якого вимкнення (S5)", "Пристрій присутній, але стан живлення не може бути визначений", "Пристрій вимкнено"];
        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="' + "Перемикач живлення готовий до використання." + '">' + "Перемикач" + '</span>');
                        } else {
                            states.push('<span title="' + "Порт IP-KVM відкритий і готовий до використання." + '">' + "IP-KVM" + '</span>');
                        }
                    } else {
                        states.push('<span title="' + "MeshAgent підключений та готовий до використання" + '">' + "Агент" + '</span>');
                    }
                }
                if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA підключено та готово до використання." + '">' + "CIRA" + '</span>'); }
                else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT маршрутизується." + '">' + "AMT" + '</span>'); }
                if ((node.conn & 8) != 0) { states.push('<span title="' + "MeshAgent доступний із використанням іншого агента ретрансляції." + '">' + "Ретрансляція" + '</span>'); }
                if ((node.conn & 16) != 0) { states.push('<span title="' + "З'єднання MQTT із пристроєм активне." + '">' + "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 "Невідомо";
        }

        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 = "Скинути вибір";
                QV('cxmdesktop', true);
            } else {
                QE('GroupActionButton', false);
                Q('SelectAllButton').value = "Обрати Все";
                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', "Безпека акаунту", "Неможливо отримати доступ до цієї властивості, доки не ввімкнено двофакторну автентифікацію. Це потрібно для додаткової безпеки. Перейдіть на вкладку \"Мій акаунт\" і перегляньте розділ \"Безпека акаунту\".");
                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: "Надіслати повідомлення MQTT" });; break; } }
            }

            // Display the options that are allowed depending on what devices are selected.
            addedOptions.push({ v: 105, name: "Експорт інформації про пристрої", 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: "Перемістити до групи пристроїв" }); }
                if (((node.conn & 1) != 0) && ((rights & 0x8000) != 0) && ((added & 1) == 0)) { added |= 1; addedOptions.push({ v: 104, name: "Видалити агент" }); }
                if ((rights & 64) && ((added & 8) == 0)) { added |= 8; addedOptions.push({ v: 100, name: "Пробудити пристрої" }); }
                if ((rights & 262144) && ((added & 4) == 0)) { added |= 4; addedOptions.push({ v: 4, name: "Перевести у режим сну" }); addedOptions.push({ v: 3, name: "Перезапуск пристроїв" }); addedOptions.push({ v: 2, name: "Вимкнути пристрої" }); }
                if ((rights & 131072) && ((added & 16) == 0)) { added |= 16; addedOptions.push({ v: 106, name: "Виконати команди" }); }
                if ((rights & 16384) && ((added & 32) == 0)) { added |= 32; addedOptions.push({ v: 108, name: "Сповіщення пристроїв" }); }
                if ((rights & 4) && ((added & 64) == 0)) { added |= 64; addedOptions.push({ v: 107, name: "Редагувати теги" }); }
                if ((rights & 8) && ((added & 256) == 0)) { added |= 256; addedOptions.push({ v: 109, name: "Відвантажити файли" }); }
                if ((rights & 32768) && ((added & 128) == 0)) { added |= 128; addedOptions.push({ v: 101, name: "Видалити пристрої" }); }
                if ((node.agent != null) && (features2 & 0x00000010) && (rights == 0xFFFFFFFF) && ((added & 512) == 0)) {
                    added |= 512;
                    addedOptions.push({ v: 110, name: "Примусове оновити агента" });
                    addedOptions.push({ v: 111, name: "Очистити ядро агента" });
                    addedOptions.push({ v: 112, name: "Відправити ядро ​​сервера за умовчанням" });
                }
            }

            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 = "Виберіть операцію, яку потрібно виконати на всіх вибраних пристроях. Дії виконаються лише при наявності відповідних прав." + '<br /><br />';
            x += addHtmlFormFloating("Операція", '<select id=d2groupop class="form-select">' + addedOptionsStr + '</select>');

            setModalContent('xxAddAgent', "Групова дія", 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 += "Підтвердити видалення відібраного пристрою?" + '<br /><br />'; }
                else {
                    x += format("Підтвердити видалення {0} відібраних пристроїв?" + '<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 відібраний пристрій у мережі." + '<br />', nconn); }
                    else if (nconn > 1) { y += format("{0} вибраних пристроїв онлайн." + '<br />', nconn); }
                    if (ndisc == 1) { y += format("1 відібраний пристрій поза мережою." + '<br />', ndisc); }
                    else if (ndisc > 1) { y += format("{0} вибраних пристроїв офлайн." + '<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() />' + "Підтвердити" + '</label>';
                setModalContent('xxAddAgent', "Видалити Пристрої", 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: "Виконати команди на вибраних пристроях.", func: uncheckAllDevices });
            } else if (op == 107) {
                // Edit tags
                var x = "Виконати групову операцію тегування пристроїв" + '<br /><br />';
                x += addHtmlFormFloating("Операція", '<select id=d2deviceop class="form-select"><option value=1>' + "Додати теги" + '</option><option value=2>' + "Установити теги" + '</option><option value=3>' + "Видалити теги" + '</option></select>');
                x += addHtmlFormFloating("Теги", '<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', "Редагувати Теги Пристрою", x);
                $('#dp10devicevalue').select2({
                    theme: 'bootstrap-5',
                    width: $( this ).data( 'width' ) ? $( this ).data( 'width' ) : $( this ).hasClass( 'w-100' ) ? '100%' : 'style',
                    placeholder: "Тег1, Тег2, Тег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>'+ "Виконати групове сповіщення пристроїв" + '</div>';
                x += '<select id=d2deviceop class="form-select-sm me-2" style=width:100%;margin-bottom:4px><option value=2>' + "Висувне Сповіщення" + '</option><option value=1>' + "Вікно повідомлення" + '</option><option value=3>' + "Вікно Попередження" + '</option></select>';
                x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Назва" + '" 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>' + "Показувати 2 хвилини (Типово)" + '</option>';
                x += '<option value=10>' + "Показувати 10 хвилин" + '</option>';
                x += '<option value=30>' + "Показувати 30 хвилин" + '</option>';
                x += '<option value=60>' + "Показувати 60 хвилин" + '</option>';
                x += '<option value=0>' + "Показувати повідомлення, доки користувач не закриє його" + '</option>';
                x += '</select>';
                setModalContent('xxAddAgent', "Сповіщення пристрою", 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 = "Відправити вибрані файли на всі вибрані пристрої" + '<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("Шлях Windows", '<input type=text class="form-control" onchange="d2batchUploadValidate()" onkeyup="d2batchUploadValidate()" name=winpath id=d2winuploadpath placeholder="C:\\temp" value="" />'); }
                if (linuxtype) { x += addHtmlFormFloating("Шлях 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 />' + "Створити теку, якщо вона не існує?" + '</label><br />';
                x += '<label><input type=checkbox class="form-check-input me-2" name=overwriteFiles />' + "Перезаписати, якщо файл існує?" + '</label></form>';
                setModalContent('xxAddAgent', "Пакетне Вивантаження Файлів", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2batchUploadValidateOk());
                d2batchUploadValidate();
            } else if (op == 110) {
                // Force agent update
                var x = "Примусово оновити агент на відібраних пристроях?" + '<br /><br />';
                setModalContent('xxAddAgent', "Примусове оновити агента", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentUpdateExec());
            } else if (op == 111) {
                // Clear agent core
                var x = "Очистити ядро агента на відібраних пристроях?" + '<br /><br />';
                setModalContent('xxAddAgent', "Очистити ядро агента", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentClearCoreExec());
            } else if (op == 112) {
                // Upload default server core
                var x = "Відправити ядро ​​сервера за замовчуванням на вибрані пристрої?" + '<br /><br />';
                setModalContent('xxAddAgent', "Відправити ядро ​​сервера за умовчанням", 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 = "Завантажте файл переліку пристроїв в одному із наведених нижче форматів." + '<br /><br />';
            x += addHtmlValue("Формат CSV", '<a href=# style=cursor:pointer onclick=\'return p2downloadDeviceInfoCSV()\'>' + 'devicelist.csv' + '</a>');
            x += addHtmlValue("Формат 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 />' + "Включити деталі пристрою" + '</label></div>';
            setModalContent('xxAddAgent', "Експорт Інформації про Пристрій", 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' : '') + '>' + "Командний Рядок 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" + '</option>'; }
                if (agenttype == true) { x += '<option value=4' + ((runopt.type == 4) ? ' selected' : '') + '>' + "Консоль Агента" + '</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' : '') + '>' + "Запустити від імені агента" + '</option><option value=1' + ((runopt.runAs == 1) ? ' selected' : '') + '>' + "Запустити від користувача або від агента(якщо користувача не існує)" + '</option><option value=2' + ((runopt.runAs == 2) ? ' selected' : '') + '>' + "Запустити примусово від користувача" + '</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' : '') + '>' + "Команди з текстового поля" + '</option><option value=1' + ((runopt.source == 1) ? ' selected' : '') + '>' + "Команди з файлу" + '</option>';
                    if (userinfo.siteadmin & 8) { x += '<option value=2' + ((runopt.source == 2) ? ' selected' : '') + '>' + "Команди з файлу на сервері" + '</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', "Виконати команди", 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(' ' + "або" + ' '), 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 = "Порт 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', "Підключення 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 = "Порт 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', "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 = "Порт віддаленого з'єднання 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', "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 = "Порт віддаленого підключення 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', "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 = "Порт віддаленого підключення 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', "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', "Перейменувати", '<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("Редактор файлів", "Можна редагувати лише файли розміром менше 200 кБ."); }
            } else if (action == 3) {
                // Delete the file
                setModalContent('xxAddAgent', "Видалити", "Видалити елемент?");
                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: "Змінити розташування вузла", callback: function (obj) { modifyMarkerloc(obj.data); } };

            // Clear Marker item
            map_cm_clearMarker = {
                text: "Видалити розташування вузла", callback: function (obj) {
                    meshserver.send({ action: 'changedevice', nodeid: obj.data.a, userloc: [] }); // Clear the user position marker
                }
            };

            // Save Marker item
            map_cm_saveMarker = { text: "Зберегти розташування вузла", callback: function (obj) { saveMarkerloc(obj.data); } };

            // Build a context menu for a feature
            map_cm_nodemenu_items = [
                { text: "Загальні відомості", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 10); } } },
                { text: "Стільниця", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 11); } } },
                { text: "Термінал", 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: "Збільшити масштаб", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 19); } },
                { text: "Зменшити масштаб", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 2); } }
            ];

            // Context menu for clicks other than on feature
            contextmenu_items = [
                { text: "Оновити", callback: function () { refreshMap(true, true); } },
                { text: "Збільшити до розміру", callback: function () { zoomToFitExtent(); } },
                { text: "Центрувати мапу тут", callback: function (obj) { xxmap.mapView.animate({ center: obj.coordinate }); } },
                { text: "Розмістити тут вузол", 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 == "Збільшити масштаб" || item.text == "Зменшити масштаб") { 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>' + "Пошук" + '</label>&nbsp&nbsp<input type=text placeholder="' + "Назва пристрою" + '" 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>' + "Пристрій не знайдено." + '</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', "Виберіть вузол для розміщення", 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>' + "Розташування не знайдено." + '<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', "Безпека акаунту", "Неможливо отримати доступ до цієї властивості, доки не буде підтверджено адресу е-пошти. Це потрібно для відновлення пароля. Перейдіть на вкладку \"Мій акаунт\", щоб змінити та підтвердити свою адресу е-пошти. ");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return;
            }

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0)) {
                setModalContent('xxAddAgent', "Безпека акаунту", "Неможливо отримати доступ до цієї властивості, доки не ввімкнено двофакторну автентифікацію. Це потрібно для додаткової безпеки. Перейдіть на вкладку \"Мій акаунт\" і перегляньте розділ \"Безпека акаунту\".");
                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 = "Пристрій підключено"; }
                    if (bat.state == 'dc') { statestr = "Пристрій живиться від батареї"; }

                    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>' + "Немає" + '</i>'; }
                if (((meshrights & 4) != 0) && ((!mesh.flags) || ((mesh.flags & 2) == 0 || (mesh.flags & 16)))) { nname = '<span tabindex=0 title="' + "Клікніть тут, щоб редагувати назву пристрою на стороні сервера" + '" 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("Ім'я", nnameEx); }

                // Attribute: Mesh
                if (mesh) { x += addDeviceAttribute('<span title="' + "Назва групи пристроїв, до якої належить цей комп'ютер." + '">' + "Група" + '</span>', '<a href=# title="' + "Назва групи пристроїв, до якої належить цей комп'ютер" + '" 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="' + "Ім'я цього комп'ютера, встановлене операційною системою" + '">' + "Назва в ОС" + '</span>', '<span title="' + "Ім'я цього комп'ютера, встановлене операційною системою" + '">' + 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("Ім'я хоста", addLinkConditional((node.host ? EscapeHtml(node.host) : ('<i>' + "Немає" + '</i>')), 'showEditNodeValueDialog(1)', meshrights & 4));
                }

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

                // IP-KVM / PDU information
                if (node.mtype == 4) {
                    if (node.portnum != null) { x += addDeviceAttribute("Номер порту", node.portnum); }
                    if (node.porttype != null) { x += addDeviceAttribute("Тип порту", node.porttype); }
                }

                // Attribute: Mesh Agent
                if ((node.agent != null) && (node.agent.id != null) && (node.mtype == 3)) {
                    if (node.agent.id == 4) { x += addDeviceAttribute("Тип Пристрою", "Windows"); }
                    if (node.agent.id == 6) { x += addDeviceAttribute("Тип Пристрою", "Linux"); }
                    if (node.agent.id == 29) { x += addDeviceAttribute("Тип Пристрою", "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 += ', ' + "Обмежено"; }
                    x += addDeviceAttribute("Mesh Agent", str);
                }

                // Attribute: Intel AMT
                if (node.intelamt != null) {
                    var str = '';
                    var provisioningStates = { 0: nobreak("Не активовано (Поперед)"), 1: nobreak("Не Активовано (всередині)"), 2: nobreak("Активовано") };
                    if (node.intelamt.ver != null && node.intelamt.state == null) { str += '<i>' + "Невідомий статус" + '</i>, v' + EscapeHtml(node.intelamt.ver); }
                    else if ((node.intelamt.ver == null) && (node.intelamt.state == 2)) { str += '<i>' + "Активовано" + '</i>'; }
                    else if ((node.intelamt.ver == null) || (node.intelamt.state == null)) { str += '<i>' + "Невідома версія та стан" + '</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 активовано в Режимі Керування Клієнтом" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { str += ' <span title="' + "Intel&reg; AMT активовано в Режимі Адміністратора" + '">' + "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 розгорнуто з безпекою мережі 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="' + "Редагувати Облікові Дані Intel&reg; AMT" + '" onclick=editDeviceAmtSettings("' + node._id + '")>' + "Немає Облікових Даних" + '</i>';
                                editUserCredentialsIcon = true;
                            } else {
                                str += ', <i style=color:#FF0000>' + "Немає Облікових Даних" + '</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 = "Хибні Облікові Дані"; }
                            if ((node.intelamt.warn & 8) != 0) { warn = "Перевірка облікових даних"; }
                            if (warn != null) {
                                if ((meshrights & 4) != 0) {
                                    str += ', <i style=color:#FF0000;cursor:pointer title="' + "Редагувати Облікові Дані 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="' + "Редагувати Облікові Дані Intel&reg; AMT" + '" style=cursor:pointer onclick=editDeviceAmtSettings("' + node._id + '")>';
                        }
                    }

                    var meName = '<span title="' + "Intel&reg; Manageability Engine" + '">' + "Intel&reg; ME" + '<span>';
                    if (typeof node.intelamt.sku == 'number') {
                        if ((node.intelamt.sku & 8) != 0) { meName = '<span title="' + "Технологія Intel&reg; Active Management" + '">' + "Intel&reg; AMT" + '<span>'; }
                        else if ((node.intelamt.sku & 16) != 0) { meName = '<span title="' + "Стандартне Управлення 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("Тег Агента", 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("Тег 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("Операційна система", EscapeHtml(node.osdesc)); }

                // Windows Security Central
                if (node.wsc) {
                    var y = [];
                    if (node.wsc.antiVirus != null) { if (node.wsc.antiVirus == 'OK') { y.push("AV" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("AV" + ' - <span style=color:red>' + "КЕПСЬКО" + '</span>'); } }
                    if (node.wsc.autoUpdate != null) { if (node.wsc.autoUpdate == 'OK') { y.push("Оновити" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Оновити" + ' - <span style=color:red>' + "КЕПСЬКО" + '</span>'); } }
                    if (node.wsc.firewall != null) { if (node.wsc.firewall == 'OK') { y.push("Фаєрвол" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Фаєрвол" + ' - <span style=color:red>' + "КЕПСЬКО" + '</span>'); } }
                    x += addDeviceAttribute("Безпека Windows", y.join(', '));
                }

                // Defender for Windows Server
                if (node.defender) {
                    var y = [];
                    if (node.defender.RealTimeProtection != null) { if (node.defender.RealTimeProtection == true) { y.push("Захист у Реальному Часі" + ' - <span style=color:green>' + "Увімк" + '</span>'); } else { y.push("Захист у Реальному Часі" + ' - <span style=color:red>' + "Вимк" + '</span>'); } }
                    if (node.defender.TamperProtected != null) { if (node.defender.TamperProtected == true) { y.push("Захист від Злому" + ' - <span style=color:green>' + "Увімк" + '</span>'); } else { y.push("Захист від Злому" + ' - <span style=color:red>' + "Вимк" + '</span>'); } }
                    if (node.defender.AntivirusSignatureVersion != null) { y.push("Версія підпису" + ' - <span style=color:green>' + EscapeHtml(node.defender.AntivirusSignatureVersion) + '</span>'); }
                    if (y.length > 0) x += addDeviceAttribute("Захисник Windows", y.join(', '));
                }

                // Linux Security Central
                if (node.lsc) {
                    var y = [];
                    if (node.lsc.antiVirus != null) { if (node.lsc.antiVirus == 'OK') { y.push("AV" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("AV" + ' - <span style=color:red>' + "КЕПСЬКО" + '</span>'); } }
                    if (node.lsc.firewall != null) { if (node.lsc.firewall == 'OK') { y.push("Фаєрвол" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Фаєрвол" + ' - <span style=color:red>' + "КЕПСЬКО" + '</span>'); } }
                    if (y.length > 0) x += addDeviceAttribute("Безпека 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>' + "Вимкнено" + '</span>'; }
                            if (node.av[i].updated !== true) { avx += ' - <span style=color:red>' + "Застаріло" + '</span>'; }
                            if ((node.av[i].enabled == true) && (node.av[i].updated == true)) { avx += ' - <span style=color:green>' + "OK" + '</span>'; }
                            y.push(avx);
                        }
                    }
                    x += addDeviceAttribute("Антивірус", 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 + ' - ' + "Заблоковано" : "Заблоковано", true); }
                        if (label) { return '<span style=cursor:default title=\'' + label + '\'>' + escapedUser + '</span>'; }
                        return escapedUser;
                    }).join(', ');
                    x += addDeviceAttribute((node.users.length > 1 ? "Користувачі Активні" : "Активний Користувач"), u);
                }

                // Windows Idle Time
                if (node.idletime && node.idletime != -1) { x += addDeviceAttribute("Час бездіяльності", 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("Запит до Стільниці та Панелі Засобів"); } else if (consent & 0x0040) { meshFeatures.push("Стільниця: панель підключення"); } else if (consent & 0x0008) { meshFeatures.push("Стільниця: запит"); } else { if (consent & 0x0001) { meshFeatures.push("Стільниця: сповістити"); } }
                    if (consent & 0x0010) { meshFeatures.push("Термінал: запит"); } else { if (consent & 0x0002) { meshFeatures.push("Термінал: сповістити"); } }
                    if (consent & 0x0020) { meshFeatures.push("Файли: запит"); } else { if (consent & 0x0004) { meshFeatures.push("Файли: сповістити"); } }
                    if (consent == 7) { meshFeatures = ["Завжди сповіщати"]; }
                    if ((consent & 56) == 56) { meshFeatures = ["Завжди запитувати"]; }

                    meshFeatures = meshFeatures.join(', ');
                    if (meshFeatures == '') { meshFeatures = '<i>' + "Немає" + '</i>'; }
                    x += addDeviceAttribute("Згода користувача", 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("Підключитись"); }
                    if (devNotify & 4) { devNotifyStr.push("Відключити"); }
                    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("е-пошта ({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("Обмін повідомленнями ({0})", xx)); }
                    }
                }
                devNotifyStr = devNotifyStr.join(', ');
                if (devNotifyStr == '') { devNotifyStr = '<i>' + "Немає" + '</i>'; }
                x += addDeviceAttribute("Сповіщення", 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="' + "MeshAgent підключений та готовий до використання" + '">' + "Mesh Agent" + '</span>');
                    if ((node.conn & 2) != 0) cstate.push('<span title="' + "Intel&reg; AMT CIRA підключено та готово до використання." + '">' + "Intel&reg; AMT CIRA" + '</span>');
                    else if ((node.conn & 4) != 0) cstate.push('<span title="' + "Intel&reg; AMT маршрутизується та готовий до використання." + '">' + "Intel&reg; AMT" + '</span>');
                    if ((node.conn & 8) != 0) cstate.push('<span title="' + "MeshAgent доступний із використанням іншого агента ретрансляції." + '">' + "Mesh Ретрансляція" + '</span>');
                    if ((node.conn & 16) != 0) { cstate.push('<span title="' + "З'єднання MQTT із пристроєм активне." + '">' + "MQTT" + '</span>'); }
                    x += addDeviceAttribute("З'єднання", cstate.join(', '));
                }

                // Node tags
                var groupingTags = '<i>' + "Немає" + '</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("Теги", '<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("Теги", '<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-Користувач+Пароль" : ((node.ssh == 2) ? "SSH-Користувач+Ключ+Пароль" : "SSH-Користувач+Ключ")) + ' <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-Користувач+Пароль" : ((node.ssh == 2) ? "SSH-Користувач+Ключ+Пароль" : "SSH-Користувач+Ключ"))); }
                        if (node.rdp != null) { y.push("RDP"); }
                    }
                    x += addDeviceAttribute("Облікові Дані", 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="' + "Групи пристроїв, для яких цей пристрій є ретранслятором" + '">' + "Ретрансляція для" + '</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="' + "Дії" + '" title="' + "Увімкнути живлення на пристрої" + '" onclick=deviceActionFunction() />'; }
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Примітки" + '" title="' + "Див. примітки щодо цього пристрою" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Журнал Подій" + '" title="' + "Записати подію для цього пристрою" + '" 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="' + "Виконати" + '" title="' + "Виконувати команди на цьому пристрої" + '" 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="' + "Повідомлення" + '" title="' + "Показ текстового повідомлення на віддаленому пристрої" + '" 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="' + "Чат" + '" title="' + "Відкрити вікно чату на цьому комп'ютері" + '" 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="' + "Поширити" + '" title="' + "Створіть посилання, щоб поділитися цим пристроєм з гостем" + '" 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="' + "Вимкнути" + '" title="' + "Вимкнути" + '" 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="' + "Увімкнути" + '" title="' + "Увімкнути" + '" onclick=setIpPduState(1) />'; }
                        }
                    } else {
                        if (meshrights & 8) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Дистанційне керування" + '" title="' + "Дистанційне керування" + '" 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="' + "Перемістити цей пристрій до іншої групи пристроїв" + '">' + "Змінити Групу" + '</a>';
                }
                if ((meshrights & 0x8000) != 0) { // MESHRIGHT_UNINSTALL
                    x += '&nbsp;<a href=# onclick=p10showDeleteNodeDialog("' + node._id + '") title="' + "Видалити цей пристрій" + '">' + "Видалити Пристрій" + '</a>';
                }
                x += '</div><div class="p10html3left">';
                if ((node.agent) && (node.mtype != 3) && ((meshrights & 1048576) != 0)) x += '<a href=# onclick=p10showNodeNetInfoDialog("' + node._id + '") title="' + "Перегляд інформації про мережевий інтерфейс пристрою" + '">' + "Інтерфейси" + '</a>&nbsp;';
                if ((features & 0x00008000) && (xxmap != null)) x += '<a href=# onclick=p10showNodeLocationDialog("' + node._id + '") title="' + "Показати інформацію про місцезнаходження пристрою" + '">' + "Локація" + '</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="' + "Маршрутизатор трафіку, який використовується для підключення до пристрою через цей сервер" + '.">' + "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="' + "Відкрити термінал 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="' + "Потрібна інсталяція MeshCentral Router" + '.">' + "RDP" + ((node.rdpport && (node.rdpport != 3389)) ? '/' + node.rdpport : '') + '</a>&nbsp;';
                                }
                            }
                        }
                        if (node.agent.id > 4) {
                            if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
                                    x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ') title="' + "Потрібно інсталювати MeshCentral Router." + '">' + "SSH" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
                                }
                            }
                            if (navigator.platform.toLowerCase() == 'win32') {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
                                    x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ') title="' + "Потрібно інсталювати MeshCentral Router." + '">' + "SCP" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
                                }
                            }
                        }
                        if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                            if ((serverinfo.devicemeshrouterlinks != null) && (Array.isArray(serverinfo.devicemeshrouterlinks.extralinks))) {
                                for (var i in serverinfo.devicemeshrouterlinks.extralinks) {
                                    var r = serverinfo.devicemeshrouterlinks.extralinks[i], p = '\"' + r.protocol + '\"';;
                                    if (doesDeviceMatchFilterTags(node, r.filter)) {
                                        if (typeof r.protocol == 'number') { p = r.protocol; } else if (r.protocol == 'http') { p = 1; } else if (r.protocol == 'https') { p = 2; } else if (r.protocol == 'rdp') { p = 3; } else if (r.protocol == 'ssh') { p = 4; } else if (r.protocol == 'scp') { p = 5; } else if (r.protocol == 'mcrdesktop') { p = 6; } else if (r.protocol == 'mcrfiles') { p = 7; }
                                        if (((p == 6) || (p == 7)) && (node.mtype != 2)) continue; // If this is not an agent device, don't show MeshCentral Router desktop/file links.
                                        x += '<a href=# onclick=p10MCRouter("' + node._id + '",' + p + ',' + r.port + (r.ip ? (',\"' + r.ip + '\"') : ',null') + ',' + (r.localport ? r.localport : 0) + ') title="' + "Потрібно інсталювати MeshCentral Router." + '">' + r.name + '</a>&nbsp;';
                                    }
                                }
                            }
                        }
                    }

                    // noVNC link
                    if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && ((features & 0x20000000) == 0)) {
                        x += '<a href=# cmenu=rfbPortContextMenu id=rfbLink onclick=p10rfb("' + node._id + '") title="' + "Розпочати вебсеанс VNC на цьому пристрої" + '.">' + "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="' + "Розпочати вебсеанс RDP на цьому пристрої" + '.">' + "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="' + "Розпочати вебсеанс SSH на цьому пристрої" + '.">' + "Web-SSH" + '</a>&nbsp;';
                    }

                    // MQTT options
                    if ((meshrights == 0xFFFFFFFF) && (features & 0x00400000)) { x += '<a href=# onclick=p10showMqttLoginDialog("' + node._id + '") title="' + "Отримати облікові дані MQTT для цього пристрою." + '">' + "Вхід у 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="' + "Агент працює на віддаленому пристрої без прав адміністратора." + '">' + "Обмежено" + '</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="' + "Перемикач підключеного порту" + '">' + "Перемикач підключеного порту" + '</span>' + agentPrivilages;
                        } else {
                            powerstate += '<span style=font-size:12px title="' + "Порт IP-KVM підключено" + '">' + "Порт IP-KVM підключено" + '</span>' + agentPrivilages;
                        }
                    } else {
                        powerstate += '<span style=font-size:12px title="' + "Агент підключено" + '">' + "Агент підключено" + '</span>' + agentPrivilages;
                    }
                }
                if ((connectivity & 2) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Підключено Intel&reg; AMT" + '">' + "Підключено Intel&reg; AMT" + '</span>'; }
                else if ((connectivity & 4) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Виявлено Intel&reg; AMT" + '">' + "Виявлено Intel&reg; AMT" + '</span>'; }
                if ((connectivity & 16) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "MQTT підключено" + '">' + "Підключено канал MQTT" + '</span>'; }
                if ((powerstate == '') && node.lastconnect) { powerstate = '<span style=font-size:12px>' + "Нещодавно був:" + '<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> ' + "Додати Користувача" + '</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> ' + "Додати Групу Користувачів" + '</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>' + "Авторизація користувача" + '</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="' + "Видалити права групи користувачів на цю групу пристроїв" + '" 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="' + "Видалити права користувача для цієї групи пристроїв" + '" 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 ? "Група користувачів" : "Користувач") + '" 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>' + "Немає користувачів зі спеціальними дозволами для пристрою" + '</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>' + "Спільний доступ до пристрою активний" + '</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="' + "Посилання для Поширення Пристрою" + '" 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="' + "Видалити поширення до пристрою" + '" class="fa-fw fa-solid fa-trash text-danger"></i>';
                        var type = ''; if (dshare.p <= 7) { type = ['', "Термінал", "Стільниця", "Стільниця + Термінал", "Файли", "Термінал + Файли", "Стільниця + Файли", "Стільниця + Термінал + Файли"][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} виконати {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} через {2} хвилини", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            } else {
                                details = format("{0}, {1} через {2} хвилин", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            }
                        }
                        if (dshare.recurring == 1) { details += ", Повторюється щодня"; }
                        if (dshare.recurring == 2) { details += ", Повторюється щотижня"; }
                        if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Лише перегляд робочого столу"; }
                        if (dshare.consent != null) {
                            if (dshare.consent == 0) { details += ", Нема Погодження"; } else {
                                if ((dshare.consent & 0x0038) != 0) { details += ", Запит на погодження"; }
                                if ((dshare.consent & 0x0040) != 0) { details += ", Панель Засобів"; }
                            }
                        }
                        var guestName = EscapeHtml(dshare.guestName);
                        if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Само-Поширення Агента" + '</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', "Потужність", 'Perform power off?');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: 2 }); });
            } else {
                // Turn on
                setModalContent('xxAddAgent', "Потужність", '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">' + "Вебсповіщення" + '</div>';
            x += '<div><label><input id=p20notifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Підключення пристрою" + '</label></div>';
            x += '<div><label><input id=p20notifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Відключення пристрою" + '</label></div>';
            if (currentNode.intelamt != null) { fx += 2; x += '<div><label><input id=p20notifyIntelAmtKvmActions type=checkbox class="form-check-input me-2" />' + "Події Intel&reg; AMT для стільниці та послідовного порту" + '</label></div>'; }
            if (fx & 1) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Сповіщення е-поштою" + '</div>';
                x += '<div><label><input id=p20enotifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Підключення пристрою" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Відключення пристрою" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceHelp type=checkbox class="form-check-input me-2" />' + "Запит допомоги" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Сповіщення месенджера" + '</div>';
                x += '<div><label><input id=p20emsgDeviceConnect type=checkbox class="form-check-input me-2" />' + "Підключення пристрою" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Відключення пристрою" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceHelp type=checkbox class="form-check-input me-2" />' + "Запит допомоги" + '</label></div>';
            }
            setModalContent('xxAddAgent', "Налаштування Сповіщень", 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 "Усі дозволи на пристрій"; }
            var str = [];
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Без введення");
                if (rights & 512) str1.push("Без Терміналу");
                if (rights & 1024) str1.push("Без файлів");
                if (rights & 2048) str1.push("Немає AMT");
                if (rights & 4096) str1.push("Лише базові клавіші");
                if (rights & 65536) str1.push("Без стільниці");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Поширити гостю");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Керування"); }
            }
            if (rights & 16) str.push("Консоль");
            if (rights & 32) str.push("Файли Сервера");
            if (rights & 64) str.push("Пробудити");
            if (rights & 128) str.push("Примітки");
            if (rights & 8192) str.push("Обмеження подій");
            if (rights & 16384) str.push("Чат");
            if (rights & 32768) str.push("Видалити");
            if (rights & 131072) str.push("Команди");
            if (rights & 262144) str.push("Перезапустити/Вимкнути");
            if (rights & 524288) str.push("Поширити");
            if (rights & 1048576) str.push("Деталі");
            if (rights & 2097152) str.push("Ретрансляція");
            if (str.length == 0) return "Немає Дозволів";
            return str.join(', ');
        }

        function makeDeviceGroupRightsString(rights) {
            if (rights == 0xFFFFFFFF) { return "Усі дозволи"; }
            var str = [];
            if (rights & 1) str.push("Редагувати Групу");
            if (rights & 2) str.push("Керувати користувачами");
            if (rights & 4) str.push("Керувати Пристроями");
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Без введення");
                if (rights & 512) str1.push("Без Терміналу");
                if (rights & 1024) str1.push("Без файлів");
                if (rights & 2048) str1.push("Немає AMT");
                if (rights & 4096) str1.push("Лише базові клавіші");
                if (rights & 65536) str1.push("Без стільниці");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Поширити гостю");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Керування"); }
            }
            if (rights & 16) str.push("Консоль");
            if (rights & 32) str.push("Файли Сервера");
            if (rights & 64) str.push("Пробудити");
            if (rights & 128) str.push("Примітки");
            if (rights & 8192) str.push("Обмеження подій");
            if (rights & 16384) str.push("Чат");
            if (rights & 32768) str.push("Видалити");
            if (rights & 131072) str.push("Команди");
            if (rights & 262144) str.push("Перезапустити/Вимкнути");
            if (rights & 524288) str.push("Поширити");
            if (rights & 1048576) str.push("Деталі");
            if (rights & 2097152) str.push("Ретрансляція");
            if (str.length == 0) return "Немає Дозволів";
            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', "Додати Подію Пристрою", '<textarea id=d2devEvent style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea><span style=font-size:10px>' + "Це додасть запис до журналу подій цього пристрою." + '<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>' + "Примітки групи пристроїв можуть переглядати та змінювати інші адміністратори цієї групи пристроїв." + '<span>'; }
            if (showNotesPanel === 'true') { x += ' <span style=font-size:10px><a target=_blank href=\'https://www.markdownguide.org/cheat-sheet/\'>' + "Синтаксис Markdown підтримується" + '</a></span>'; }
            setModalContent('xxAddAgent', "Примітки", 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', "Відкрити Сторінку на Пристрої", '<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>' + "Показ вікна повідомлення на віддаленому пристрої." + '</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>' + "Показувати 2 хвилини (Типово)" + '</option>';
            x += '<option value=10>' + "Показувати 10 хвилин" + '</option>';
            x += '<option value=30>' + "Показувати 30 хвилин" + '</option>';
            x += '<option value=60>' + "Показувати 60 хвилин" + '</option>';
            x += '<option value=0>' + "Показувати повідомлення, доки користувач не закриє його" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Повідомлення Пристрою", 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', "Віддалене Блокування Введення", (value == 1) ? "Заблокувати мишу та клавіатуру віддаленого користувача?" : "Розблокувати мишу та клавіатуру віддаленого користувача?");
            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>' + "Екранна клавіатура"
                            + ' <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',         "Вийти з повноекранного режиму",                          'deskToggleFull();deskCloseMobileActions()',                       'text-danger') +
                monitorRow +
                row(cls, (_mobileZoomMode==='native' ? 'fa-compress-arrows-alt' : 'fa-expand-arrows-alt'),
                    (_mobileZoomMode==='native' ? "Масштабувати до розміру вікна" : "Рідний розмір (з прокруткою)"),  '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',          "Екранна клавіатура" + ' <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 ? "Вимкнути режим тачпада" : "Увімкнути режим тачпада"),  'deskToggleTrackpad();deskCloseMobileActions()') +
                row(cls, 'fa-hand-pointer',      (swap ? "Вимкнути режим правої кнопки миші" : "Увімкнути режим правої кнопки миші"),   'deskToggleRightClickMode();deskCloseMobileActions()') +
                row(cls, 'fa-font',              "Ввести текст",                                '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',           "Гарячі клавіші",                       'deskCustomizeKeys();deskCloseMobileActions()') +
                row(cls, 'fa-paste',             "Буфер Обміну",                               'showDeskClip();deskCloseMobileActions()') +
                row(cls, 'fa-clipboard',         "Завантажити буфер обміну",                         'deskClipboardOutFunction();deskCloseMobileActions()') +
                row(cls, 'fa-rotate-right',      "Прокрутити праворуч",                             'drotate(1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate-left',       "Прокрутити ліворуч",                              'drotate(-1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate',            "Оновити робочий стіл",                          'deskRefreshFunction();deskCloseMobileActions()') +
                row(cls, 'fa-gear',              "Налаштування робочого столу",                         'showDesktopSettings();deskCloseMobileActions()') +
                row(end, 'fa-plug-circle-xmark', "Відключити",                               '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>' + "Висувне Сповіщення" + '</option><option value=1>' + "Вікно повідомлення" + '</option><option value=3>' + "Вікно Попередження" + '</option></select>';
            x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Назва" + '" 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>' + "Показувати 2 хвилини (Типово)" + '</option>';
            x += '<option value=10>' + "Показувати 10 хвилин" + '</option>';
            x += '<option value=30>' + "Показувати 30 хвилин" + '</option>';
            x += '<option value=60>' + "Показувати 60 хвилин" + '</option>';
            x += '<option value=0>' + "Показувати повідомлення, доки користувач не закриє його" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Сповіщення пристрою", 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', "Заблокувати Стільницю", "Заблокувати стільницю користувача?");
                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 = "Створює з'єднання, яке дозволяє гостю без акаунту віддалено керувати цим пристроєм впродовж обмеженого часу." + '<br /><br />';
            x += addHtmlFormFloating("Ім'я Гостя", '<input id=d2inviteName class="form-control" maxlength=128 type=text onkeyup=showShareDeviceValidate() />');
            var deskFull = '<option value=2>' + "Стільниця" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x100) != 0)) { deskFull = ''; }
            var fullTerm = '<option value=1>' + "Термінал" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x200) != 0)) { fullTerm = ''; }
            var fullFiles = '<option value=4>' + "Файли" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x400) != 0)) { fullFiles = ''; }
            var deskFiles = '<option value=5>' + "Стільниця + Файли" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x500) != 0)) { deskFiles = ''; }
            var termFiles = '<option value=6>' + "Термінал + Файли" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x600) != 0)) { termFiles = ''; }
            var allFeatures = '<option value=7>' + "Стільниця + Термінал + Файли" + '</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>' + "Стільниця, лише Перегляд" + '</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("Тип", '<select id=d2shareType class="form-select" onchange=showShareDeviceValidate()>' + y + '</select>');
            var options = { 1: "1 хвилина", 5: "5 хвилин", 10: "10 хвилин", 15: "15 хвилин", 30: "30 хвилин", 45: "45 хвилин", 60: "60 хвилин", 120: "2 години", 240: "4 години", 480: "8 годин", 720: "12 годин", 960: "16 годин", 1440: "24 години", 2880: "2 дні", 5760: "4 дні", 0: "Необмежено" }
            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("Валідний", '<select id=d2timeRange class="form-select" onchange=showShareDeviceValidate()><option value=0>' + "Запустити зараз" + '</option><option value=1>' + "Діапазон часу" + '</option><option value=2>' + "Повторювати щодня" + '</option><option value=3>' + "Повторювати щотижня" + '</option></select>');
            x += '<div id=d2modenow>';
            x += addHtmlFormFloating("Термін придатності", '<select id=d2inviteExpire class="form-select">' + y + '</select>');
            x += '</div><div id=d2moderange style=display:none>';
            x += addHtmlFormFloating("Початок діапазону часу", '<input id=d2timeRangeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Оберіть Дату та Час..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Кінець діапазону часу", '<input id=d2timeRangeEndSelector class="flatpickr form-select" type="text" placeholder="' + "Оберіть Дату та Час..." + '" data-id="altinput">');
            x += '</div><div id=d2moderecurring style=display:none>';
            x += addHtmlFormFloating("Час запуску", '<input id=d2timeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Оберіть Дату та Час..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Тривалість", '<select id=d2inviteDuration class="form-select">' + z + '</select>');
            x += '</div>';
            if (currentNode.agent.caps & 1) { x += '<div id=d2userConsentSelector>' + addHtmlFormFloating("Згода користувача", '<select id=d2userConsent class="form-select"><option value=0>' + "Лише Сповіщати" + '<option value=1>' + "Запитати згоди" + '</option><option value=2>' + "Нема Згоди" + '</option></select>') + '</div>'; }
            x += '<div id=d2httpPortSelector>' + addHtmlFormFloating("Порт", '<input id=d2httpPort class="form-control" value=80 onkeyup=showShareDeviceValidate()></input>') + '</div>';
            x += '<div id=d2httpsPortSelector>' + addHtmlFormFloating("Порт", '<input id=d2httpsPort class="form-control" value=443 onkeyup=showShareDeviceValidate()></input>') + '</div>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Надати доступ до пристрою", 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 = "Виберіть операцію, яку ви хочете виконати на цьому пристрої." + '<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>' + "Спалах" + '</option>';
                    y += '<option value=401>' + "Вібрувати" + '</option>';
                    z += '<div id=d2devicetimediv>' + addHtmlFormFloating("Період", '<select id=d2devicetime class=form-select><option value=1000>' + "1 секунда" + '</option><option value=5000>' + "5 секунд" + '</option><option value=10000>' + "10 секунд" + '</option></select>') + '</div>';
                }
            } else {
                if ((rights & 64) != 0) { count++; y += '<option value=100>' + "Пробудити" + '</option>'; } // Wake-up permission
                if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Виконати команди" + '</option>'; } // Remote command permission
                if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Сон" + '</option><option value=3>' + "Перезапустити" + '</option><option value=2>' + "Вимкнути" + '</option>'; }
                if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Надіслати повідомлення MQTT" + '</option>'; }
                if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
                    count++;
                    y += '<option value=310>' + "Скинути Intel&reg; AMT" + '</option>';
                    y += '<option value=308>' + "Вимкнути через Intel&reg; AMT" + '</option>';
                    if ((xxcurrentView == 11) || (xxcurrentView == 12)) { // Only show these options on terminal or desktop tabs
                        y += '<option value=312>' + "Скинути BIOS через Intel&reg; AMT" + '</option>';
                        y += '<option value=311>' + "Завантаження у BIOS через Intel&reg; AMT" + '</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>' + "Увімкнути через 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 Одним Кліком" + '</option>'; } // CIRA (2) or AMT (4) connected
                if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Видалити агент" + '</option>'; }
            }
            y += '</select>';
            x += addHtmlFormFloating("Операція", y);
            if (count == 0) { x = "Наразі немає доступних дій для цього пристрою."; }
            xxdialogButtons = (count == 0) ? 2 : 3; xxdialogTag = x + z;
            setModalContent('xxAddAgent', "Дія Пристрою", 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 = "Виконати команди на вибраних пристроях." + '<br />';
                    if (wintype == true) {
                        x += '<select id=d2cmdtype style=width:100%;margin-bottom:4px;margin-top:4px> class=form-select>';
                        x += '<option value=1>' + "Командний Рядок Windows" + '</option><option value=2>' + "Windows PowerShell" + '</option>';
                        if (linuxtype == true) { x += '<option value=3>' + "Командна Оболонка Linux/BSD/macOS" + '</option>'; }
                        x += '</select>';
                    }
                    x += '<select id=d2cmduser style=width:100%;margin-bottom:4px><option value=0>' + "Запустити від імені агента" + '</option><option value=1>' + "Запустити від користувача або від агента(якщо користувача не існує)" + '</option><option value=2>' + "Запустити примусово від користувача" + '</option></select>';
                    x += '<textarea id=d2runcmd style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea>';
                    setModalContent('xxAddAgent', "Виконати команди", 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 Одним Кліком", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deviceActionOneClickRecovery());
                d3init();
            } else if (op == 302) { // Intel AMT power on
                setModalContent('xxAddAgent', "Операції з живленням у Intel&reg; AMT", "Увімкнути живлення 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', "Операції з живленням у Intel&reg; AMT", "Вимкнути живлення через Intel&reg; AMT?<br><br><b>ПРИМІТКА: якщо є активний сеанс AMT, команда вимкнення живлення буде відхилена, бо ви повинні спочатку від’єднати сеанси AMT!");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) }); });
            } else if (op == 310) { // Intel AMT reset
                setModalContent('xxAddAgent', "Операції з живленням у Intel&reg; AMT", "Виконати скидання 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', "Операції з живленням у Intel&reg; AMT", "Увімкнути живлення Intel&reg; AMT у 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', "Операції з живленням у Intel&reg; AMT", "Виконати скидання Intel&reg; AMT у 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} від {1} до {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>' + "День" + '</th><th scope=col>' + "Стан живлення за останні 7 днів" + '<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="' + "Отримати події електроживлення" + '" 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("Ім'я користувача", '<input id=dp10username class="form-control" maxlength=32 autocomplete=nope placeholder="admin" onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            x += addHtmlFormFloating("Пароль", '<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("Безпека", '<select id=dp10tls class="form-select"><option value=0>' + "Немає безпеки TLS" + '</option><option value=1>' + "Необхідна безпека TLS" + '</option></select>'); }
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { buttons = 7; }
            setModalContent('xxAddAgent', "Редагувати Облікові Дані 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("Тема", '<input id=dp2topic class="form-control" maxlength=64 onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1) />');
            x += addHtmlFormFloating("Повідомлення", '<textarea id=dp2msg class="form-control" maxlength=4096 style=width:100%;height:150px;resize:none onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1)></textarea>');
            setModalContent('xxAddAgent', "Надіслати повідомлення 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("Ви впевнені, що бажаєте видалити відібрані агенти {0}?", nodeids.length); } else { x = "Ви впевнені, що хочете видалити відібраний агент?"; }
            x += '<br /><br />';
            if (nodeids.length > 1) { x += "Це не призведе до видалення пристроїв із сервера, але пристрої більше не зможуть підключитися до сервера. Віддалений доступ до пристроїв буде втрачено. Пристрої мають бути підключеними, щоб ця команда виконалась."; } else { x += "Це не видалить цей пристрій із сервера, але пристрій більше не зможе підключитися до сервера. Віддалений доступ до пристрою буде втрачено. Пристрій має бути підключеним, щоб ця команда виконалась."; }
            x += '<br /><br /><label style=color:red><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Підтвердити" + '</label>';
            setModalContent('xxAddAgent', "Видалити агента", 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) ? ("Виберіть нову групу для цього пристрою" + '<br /><br />') : ("Виберіть нову групу для вибраних пристроїв" + '<br /><br />');
                x += addHtmlFormFloating("Нова Група Пристроїв", y);
                setModalContent('xxAddAgent', "Змінити Групу", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showChangeGroupDialogEx(3, nodeids));
            } else {
                setModalContent('xxAddAgent', "Змінити Групу", '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("Ви впевнені, що хочете видалити вузол {0}?", EscapeHtml(currentNode.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Підтвердити" + '</label>';
            xxdialogButtons = 3; xxdialogTag = nodeid;
            setModalContent('xxAddAgent', "Видалити Вузол", 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', "Локація Пристрою", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            return false;
        }

        // Show network interfaces
        function p10showNodeNetInfoDialog() {
            if (xxdialogMode) return false;
            xxdialogButtons = 1; xxdialogTag = 'if' + currentNode._id;
            setModalContent('xxAddAgent', "Мережеві інтерфейси", '<div id=d2netinfo>' + "Завантаження..." + '</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 — це програма для ретрансляції локальних портів до віддалених пристроїв (port mapping). За допомогою неї ви можете, наприклад, підключитися по RDP або VNC до пристрою використовуючи тунель через цей сервер та агент вибраного девайсу. При цьому з'єднання між Router, сервером та агентом буде зашифроване." + '</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">' + "Запустіть MeshCentral Router і натисніть \"інсталювати\" щоб з'явилась можливість його відкриття через браузер." + '</div>';
            x += '<br /><a style=cursor:pointer target="mcrouterframe" rel="noreferrer noopener" href="' + url + '"><input type=button style=width:100%;cursor:pointer value="' + "Запустити MeshCentral Router" + '" class="btn btn-primary btn-block" /></a>';
            x += '<iframe class="d-none" name="mcrouterframe"></iframe>';
            setModalContent('xxAddAgent', "MeshCentral Router", 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 (64-біт)" + '</option>';
            y += '<option value=3>' + "Windows x86 (32-біт)" + '</option>';
            y += '<option value=43>' + "Windows ARM (64-біт)" + '</option>';
            y += '<option value=6>' + "Linux x86_64 (64-біт)" + '</option>';
            y += '<option value=5>' + "Linux x86 (32-біт)" + '</option>';
            y += '<option value=16>' + "macOS x86-64 (64-біт)" + '</option>';
            y += '<option value=29>' + "macOS ARM (64-біт)" + '</option>';
            y += '<option value=25>' + "Linux ARM, Raspberry Pi (32-біт)" + '</option>';
            y += '<option value=26>' + "Linux ARM, Raspberry Pi (64-біт)" + '</option>';
            y += '</select>';

            var x = '';
            if (mode == 0) { x += '<div>' + "MeshCmd — це утиліта командного рядка, яка дозволяє виконати багато різних операцій. Ви також можете завантажити та відредагувати файл дій у якому міститиметься інформація про сервер та облікові дані." + '<br /><br />'; }
            if (mode == 1) { x += '<div>' + "Завантажте \"meshcmd\" із файлом операцій, щоб спрямувати трафік через сервер на цей пристрій. Обов’язково відредагуйте meshaction.txt і додайте пароль від свого акаунту, та внесіть інші зміни за потребою." + '<br /><br />'; }
            x += addHtmlFormFloating("Операційна система", 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("Файл Операцій", '<a onclick=downloadFile("meshagents?meshaction=generic' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary>' + "MeshAction (.txt)" + '</a>'); }
            if (mode == 1) { x += addHtmlValue("Файл Операцій", '<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', "Завантажити 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 (Win x86 32-бітна програма)"; }
            if (os == 4) { osn = "MeshCmd (Win x86-64 програма)"; }
            if (os == 43) { osn = "MeshCmd (Win ARM-64 програма)"; }
            if (os == 5) { osn = "MeshCmd (Linux x86, 32-біт)"; }
            if (os == 6) { osn = "MeshCmd (Linux x86_64, 64-біт)"; }
            if (os == 16) { osn = "MeshCmd (macOS x86-64)"; }
            if (os == 29) { osn = "MeshCmd (macOS, ARM-64-біт)"; }
            if (os == 25) { osn = "MeshCmd (Linux ARM, 32-біт)"; }
            if (os == 26) { osn = "MeshCmd (Linux ARM, 64-біт)"; }
            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', "Вибір Іконки", 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', "Редагувати Пристрій", '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', "Редагувати Пристрій", '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 = ["Назва Пристрою", "Ім'я хоста", "Опис", "Теги"];
        var showEditNodeValueDialog_modes2 = ['name', 'host', 'desc', 'tags'];
        var showEditNodeValueDialog_modes3 = ['', '', '', "Тег1, Тег2, Тег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', "Редагувати Пристрій", x);
                if (Array.isArray(v)) { v = v.join(', '); }
                Q('dp10devicevalue').value = v;
            }
            p10editdevicevalueValidate();
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { showEditNodeValueDialogEx(3, mode); });
            Q('dp10devicevalue').focus();
        }

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

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

        //
        // DESKTOP
        //

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

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

            updateMetadata(desktop, 'deskmetadata');
        }

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

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

            // Enable buttons
            var inputAllowed = ((features2 & 0x2000) == 0) && ((currentNode.agent == null) || (currentNode.agent.id != 14)) && ((rights == 0xFFFFFFFF) || (((rights & 8) != 0) && ((rights & 256) == 0) && ((rights & 4096) == 0)));
            var online = ((currentNode.conn & 1) != 0); // If Agent (1) connected, enable remote desktop
            QE('connectbutton1', online); QE('connectbutton1d', online); // Desktop Connect button and dropdown
            QE('connectbutton1r', online || (currentNode.mtype == 3));  QE('connectbutton1rd', online || (currentNode.mtype == 3)); // Desktop RDP Connect button and dropdown
            if (currentNode.rdpport && currentNode.rdpport != 3389) {
                QH('desktopCustomUpperRight', '<a style="cursor:pointer;line-height:22px" onclick="cmaltportaction(1,event)">' + format("RDP порт {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 = ['', "Очікування доступу від користувача...", "Відмовлено", "Не вдалося розпочати сесію віддаленого терміналу, {0} ({1})", "Час минув", "Отримано недійсні дані мережі", "Не вдається захопити дисплей", "Помилка узгодження протоколу ({0})", "NLA не підтримується", "Сервер вимагає SSL сертифікат", "SSL не дозволено сервером", "Сертифікат SSL відсутній на сервері", "Несумісні прапорці", "Сервер потребує гібридний режим", "SSL з авторизацією користувача вимагає сервер"];
        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("Канал даних", {}); // { 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("Облікові Дані", '<select id=d2mode class="form-select" onchange=askRdpCredentialsValidate()><option value=1>' + "Використовуйте акредитивний сервер" + '</option><option value=2>' + "Використовувати нові облікові дані" + '</option></select>');
                x += '<div id=d2cred style=display:none>';
            } else {
                x += '<div id=d2cred>';
            }
            x += addHtmlFormFloating("Домен", '<input type=text id=d2domain class="form-control" maxlength=128 />');
            x += addHtmlFormFloating("Ім'я користувача", '<input type=text id=d2user class="form-control" onKeyUp=askRdpCredentialsValidate() maxlength=128 />');
            x += addHtmlFormFloating("Пароль", '<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>' + "Запам'ятати облікові дані" + '</label>'); }
            x += '</div>';
            x += MoreStart();
            x += addHtmlFormFloating("Alt Оболонка", '<input type=text id=d2altshell class="form-control" maxlength=2048 />');
            x += addHtmlFormFloating("Робоча тека", '<input type=text id=d2workdir class="form-control" maxlength=2048 />');
            x += MoreEnd();
            setModalContent('xxAddAgent', "Облікові дані 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} стежень(ння)", 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("Час запуску", 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 підключення" : format("{0} підключень", conn.metadata.users[i]);
                        x += addHtmlValue2(getUserName(i), val);
                    }
                }
                xxdialogTag = 'sessionMetadata' + cid;
                setModalContent('xxAddAgent', "Інформація про сеанс", 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 = "Фокусувати Все";
                    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', "Налаштування Віддаленої Стільниці", 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 = "Фокусувати Все"; }
                    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 = ["Фокусувати Все", "Малий фокус", "Великий Фокус"][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: "BackSpace", 9: "Вкладка", 13: "Enter", 27: "Escape", 32: "Простір", 44: "Знімок Екрану", 45: "Вставити", 46: "Del", 36: "Домівка", 35: "Кінець", 33: "Page Up", 34: "Page Down", 37: "Ліворуч", 38: "Вгору", 39: "Вірно", 40: "Вниз", 0: "Немає" }

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

        // Customize keyboard shortcuts
        function deskCustomizeKeys() {
            if (xxdialogMode) return;
            // Shortcut list
            var x = '<div id="d2shortcuts" class="list-group mb-3" style="max-height:200px;overflow-y:auto"></div>';
            // Modifier checkboxes
            x += '<div class="d-flex flex-wrap gap-3 mb-2">';
            x += '<div class="form-check"><input id="d1kshift" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kshift">Shift</label></div>';
            x += '<div class="form-check"><input id="d1kalt"   type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kalt">Alt</label></div>';
            x += '<div class="form-check"><input id="d1kctrl"  type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kctrl">Ctrl</label></div>';
            x += '<div class="form-check"><input id="d1kwin"   type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kwin">Win</label></div>';
            x += '</div>';
            // Key select + Add button
            x += '<div class="d-flex gap-2 mb-3">';
            x += '<select id="d2keySelect" class="form-select form-select-sm">';
            for (var i in keyStrings) { x += '<option value=' + i + '>' + keyStrings[i] + '</option>'; }
            for (var i = 1; i <= 12; i++) { x += '<option value=' + (i + 111) + '>F' + i + '</option>'; }
            for (var i = 0; i < 10; i++) { x += '<option value=' + (i + 48) + '>' + i + '</option>'; }
            for (var i = 0; i < 26; i++) { x += '<option value=' + (i + 65) + '>' + String.fromCharCode(i + 65) + '</option>'; }
            x += '</select>';
            x += '<button type="button" class="btn btn-primary btn-sm flex-shrink-0" onclick="addDeskCustomizeKey()">Add</button>';
            x += '</div>';
            // Restore defaults
            x += '<button type="button" class="btn btn-outline-secondary btn-sm w-100" onclick="restoreDeskCustomizeKey()">Restore Default Shortcuts</button>';
            setModalContent('xxAddAgent', "Гарячі клавіші", 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("Ім'я", '<input type=text id=d2shortcutname class="form-control" maxlength=32 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating("Значення", '<input type=text id=d2shortcutvalue class="form-control" maxlength=256 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating('', '<input id=d2addbutton type=button value=' + "Додати" + ' onclick=addDeskCustomizeString() class="form-control" />');
            x + '</div>';
            setModalContent('xxAddAgent', "Налаштування Рядків Клавіатури", 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>' + "Не визначено рядки клавіатури" + '</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>' + "Наберіть текст і клікніть ОК, щоб ввести його дистанційно. Перш ніж продовжити, переконайтеся, що віддалений курсор встановлено у правильну позицію." + '<div>';
            x += '<textarea id=d2typeText style="margin-top:5px;width:100%;height:184px;resize:none" maxlength=2000>' + (text ? EscapeHtml(text) : '') + '</textarea>';
            setModalContent('xxAddAgent', "Віддалене Введення з Клавіатури", 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="' + "Одержати буфер обміну" + '" onclick=showDeskClipGet()></div>';
            if ((features2 & 0x1000) == 0) x += '<div class="col"><input class="btn btn-secondary" id=dlgClipSet type=button value="' + "Установити буфер" + '" 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>' + "Віддалений буфер дійсний впродовж 60 секунд." + '</span></div></div></div>';
            x += '</div>'
            xxdialogTag = 'clipboard';
            setModalContent('xxAddAgent', "Віддалений Буфер Обміну", 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="' + "Зупинити процес" + '" 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("Деталі Процесу, #{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 = "Зупинено"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Виконується"; }
                        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("Ім'я", service.name); }
                    if (service.displayName) { x += addHtmlValue("Показ назви", service.displayName); }
                    if (service.status) {
                        var ss = capitalizeFirstLetter(service.status.state.toLowerCase());
                        if (ss == 'Stopped') { ss = "Зупинено"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Виконується"; }
                        if (service.status.state) { x += addHtmlValue("Стан", ss); }
                        if (service.status.pid) { x += addHtmlValue("PID", service.status.pid); }
                        var serviceTypes = [];
                        if (service.status.isFileSystemDriver === true) { serviceTypes.push("Драйвер файлової системи"); }
                        if (service.status.isInteractive === true) { serviceTypes.push("Інтерактивний"); }
                        if (service.status.isKernelDriver === true) { serviceTypes.push("Драйвер ядра"); }
                        if (service.status.isOwnProcess === true) { serviceTypes.push("Власний процес"); }
                        if (service.status.isSharedProcess === true) { serviceTypes.push("Поширений процес"); }
                        if (serviceTypes.length > 0) { x += addHtmlValue("Тип", serviceTypes.join(', ')); }
                    }
                    if (service.startType) { x += addHtmlValue("Тип запуску", service.startType); }
                    if (service.user) { x += addHtmlValue("Користувач", service.user); }
                    if (service.installedBy) { x += addHtmlValue("Інсталював", service.installedBy); }
                    if (service.installedDate) { x += addHtmlValue("Дата Інсталяції", 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("Перезапустити лічильник помилок після ", abc + ' Day' + (abc != 1 ? 's' : ''));
                        }
                        if (service.failureActions.actions) {
                            if (service.failureActions.actions[0]) { x += addHtmlValue("Перша Невдача", service.failureActions.actions[0].type); }
                            if (service.failureActions.actions[1]) { x += addHtmlValue("Друга Невдача", service.failureActions.actions[1].type); }
                            if (service.failureActions.actions[2]) { x += addHtmlValue("Подальші Невдачі", service.failureActions.actions[2].type); }
                        }
                    }
                    x += '<br/><div style=float:right;margin-bottom:12px><input type=button value="' + "Закрити" + '" onclick=showServiceDetailsDialogEx(0,' + index + ')></div><div style=margin-bottom:12px><input type=button value="' + "Запуск" + '" onclick=showServiceDetailsDialogEx(1,' + index + ')><input type=button value="' + "Стоп" + '" onclick=showServiceDetailsDialogEx(2,' + index + ')><input type=button value="' + "Перезапустити" + '" onclick=showServiceDetailsDialogEx(3,' + index + ')></div>';
                    xxdialogTag = "service_" + index;
                    setModalContent('xxAddAgent', "Деталі сервісу", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            }
        }

        function showServiceWaitDialog(index) {
            if (xxdialogMode) return false;
            var service = deskTools.services[index];
            if (service != null) {
                meshserver.send({ action: 'msg', type: 'service', nodeid: currentNode._id, serviceName: service.name });
                xxdialogTag = "service_" + index;
                setModalContent('xxAddAgent', "Деталі сервісу", '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 = "Сесія Стільниці" + '-' + 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 = "Стільниця" + '-' + 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 = "Усі Дисплеї"; allDisplays = 2; } // Language translation
                if (str.startsWith('Display ')) { str = format("Показ {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>' + "Екранна клавіатура"
                    + ' <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', "Керування процесом", format("Зупинити процес #{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("Порт 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("Автентифікація", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=3 selected>' + "Збережений ключ" + '</option><option value=1>' + "Ім'я користувача та пароль" + '</option><option value=2>' + "Ім'я користувача та ключ" + '</option></select>');
            } else {
                x += addHtmlFormFloating("Автентифікація", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=1 selected>' + "Ім'я користувача та пароль" + '</option><option value=2>' + "Ім'я користувача та ключ" + '</option></select>');
            }
            x += '<div id=d2userauth style=display:none>';
            x += addHtmlFormFloating("Ім'я користувача", '<input id=dp2user class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
            x += '</div>';
            x += '<div id=d2passauth style=display:none>';
            x += addHtmlFormFloating("Пароль", '<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">' + "Запам'ятати облікові дані" + '</label>'); }
            x += '</div><div id=d2keyauth style=display:none>';
            x += addHtmlValue("Файл Ключа", '<input type=file id=dp2key class="form-control" maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />' + '<div id=d2badkey style=font-size:x-small>' + "Файл ключа мусить бути у форматі OpenSSH." + '</div>');
            x += addHtmlFormFloating("Пароль Ключа", '<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)>' + "Запам'ятати користувача та ключ" + '</label>');
                x += addHtmlValue('', '<label><input id=dp2keep2 type=checkbox class="form-check-input me-2">' + "Запам'ятати пароль" + '</label>');
            }
            x += '</div>';
            if (j.askkeypass) {
                x += '<div id=d2keyauth2 style=display:none>';
                x += addHtmlFormFloating("Пароль", '<input type=password id=dp2keypass2 class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
                x += '</div>';
            }
            setModalContent('xxAddAgent', "Автентифікація", 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("Помилка Підключення", 5000); break; }
                        case 'autherror': { p12setConsoleMsg("Помилка Автентифікації", 5000); break; }
                        case 'sessionerror': { p12setConsoleMsg("Тайм-аут сеансу минув", 5000); break; }
                        case 'sessiontimeout': { p12setConsoleMsg("Тайм-аут сеансу", 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 = ["Термінал UTF8", "Розширений ASCII", "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)", "Замінити (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', "Вставити", '<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>' + "Файл не знайдено" + '</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("Помилка Автентифікації", 5000); return; }
                case 'connectionerror': { p13setConsoleMsg("Помилка Підключення", 5000); return; }
                case 'sessionerror': { p13setConsoleMsg("Тайм-аут сеансу минув", 5000); return; }
                case 'sessiontimeout': { p13setConsoleMsg("Тайм-аут сеансу", 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', "Файлова Операція", '<div style=margin:10px>' + "Стиснення файлів..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13fileOperationDialogEx(10));
                } else if ((data.msg == 'unzipping') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Файлова Операція", '<div style=margin:10px>' + "Розпаковування файлу..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else if ((data.msg == 'unziperror') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Файлова Операція", '<div style=margin:10px>' + "Помилка розпаковування" + '</div><br />' + EscapeHtml(data.error));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else if ((data.msg == 'zippingFile') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Файлова Операція", '<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)">' + "Root" + '</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', "Відкрити Файл/Теку", "Ви впевнені, що бажаєте відкрити цей файл/теку на робочому столі віддаленого пристрою?");
            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', "Перейти до Теки", '<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("Фільтр", '<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=' + "Закрити" + ' style=float:right;width:80px;margin-left:5px onclick=p13findfileCancel()>';
            x += '<input id=filefind_dlgOkButton type=submit value=' + "Пошук" + ' style=float:right;width:80px onclick=p13findfileEx()><br /><br /></div>';
            setModalContent('xxAddAgent', "Знайти Файли", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            p13findfileValidate();
            Q('d2findFilter').focus();
        }

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

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

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

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

        function p13setActions() {
            var advancedFeatures = ((currentNode.agent != null) && (currentNode.agent.id != 14)); // Reduce file feature on some devices.
            if (p13filetree == null) {
                QE('p13DeleteFileButton', false);
                QE('p13NewFolderButton', false);
                QE('p13UploadButton', false);
                QE('p13NewFileButton', false);
                QE('p13RenameFileButton', false);
                QE('p13ViewFileButton', false);
                QE('p13SelectAllButton', false);
                Q('p13SelectAllButton').value = "Обрати Все";
                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 ? "Скинути вибір" : "Обрати Все");
                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("Порт 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', "Нова Тека", '<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', "Новий Файл", '<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>' + "Рекурсивне видалення" + '</label><br>' : '<input type=checkbox class="form-check-input me-2" id=p13recdeleteinput style=\'display:none\'>';
            setModalContent('xxAddAgent', "Видалити", (cc > 1) ? (format("Видалити {0} відібраний(их) елемент(ів)?", cc) + rec) : ("Видалити відібраний елемент?" + 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', "Перейменувати", '<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', "Відвантажити файл", '<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("Редактор файлів", "Можна редагувати лише файли розміром менше 200 кБ."); }
                    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', "Розпакувати до теки", '<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', "Назва 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("Підтвердити копіювання {0} записів до цієї локації?", p13clipboard.length); } else { x = format("Підтвердити копіювання 1 запису до цієї локації?"); }
                } else {
                    if (p13clipboard.length > 1) { x = format("Підтвердити переміщення {0} записів до цієї локації?", p13clipboard.length); } else { x = format("Підтвердити переміщення 1 запису до цієї локації?"); }
                }
            }
            setModalContent('xxAddAgent', "Вставити", 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("Утримано {0} записів для копіювання" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Очистити" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Утримати 1 запис для копіювання" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Очистити" + '</a>.');
                    }
                } else {
                    if (p13clipboard.length > 1) {
                        x = format("Утримано {0} записів для переміщення" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Очистити" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Утримано 1 запис для переміщення" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Очистити" + '</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', "Завантажити Файл", '<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 = ["Перенесення: УВІМК", "Перенесення: ВИМК"][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 = ["Розмір: 100%", "Розмір: 125%", "Розмір: 150%", "Розмір: 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 = ["Кодування: RAW", "Кодування: UTF8"][d4EditEncodingVal];
            putstore('editorEncoding', d4EditEncodingVal);
        }

        function d4ToggleLineBreak(update) {
            if (!update) {
                d4EditLineBreakVal = ++d4EditLineBreakVal % 3;
            }
            Q('d4LineBreakButton').value = ["Кінець рядка: Windows (CR LF)", "Кінець рядка: Linux (LF)", "Кінець рядка: 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', "Відвантажити файл", format((overWriteCount == 1) ? "Передавання перезапише 1 файл. Продовжити?" : "Передавання перезапише {0} файлів. Продовжити?", 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', "Відвантажити файл", '<div id=p13dfileName>' + "Підключення..." + '</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="' + "Це гостьовий сеанс спільного доступу" + '">' + 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>' + "Подій не знайдено" + '</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', "Деталі Події", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
        }

        //
        // DEVICE DETAILS
        //

        var deviceDetailsStats = null;
        var deviceDetailsStatsTimer = null;
        var deviceDetailsStatsData = [];
        var deviceDetailsConfig = {
            type: 'line',
            data: {
                labels: [],
                datasets: [
                    { label: "ЦП", backgroundColor: 'rgba(134, 16, 158, .5)', borderColor: 'rgb(134, 16, 158)', data: [], fill: true },
                    { label: "Пам'ять", 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("Ім'я", EscapeHtml(node.rname), s); }
            if (hardware.windows && hardware.windows.osinfo && hardware.windows.osinfo.Description) { x += addDetailItem("Опис", EscapeHtml(hardware.windows.osinfo.Description), s); }
            if (node.osdesc) { x += addDetailItem("Версія", EscapeHtml(node.osdesc), s); }
            if (hardware.windows && hardware.windows.osinfo) {
                var m = hardware.windows.osinfo;
				if(m.BuildRevision){ x += addDetailItem("Номер збірки", EscapeHtml(m.BuildRevision), s); }
                if (m.OSArchitecture) {
                    if (m.OSArchitecture.startsWith('32')) { x += addDetailItem("Архітектура", "32-біта", s); }
                    else if (m.OSArchitecture.startsWith('64')) { x += addDetailItem("Архітектура", "64-bit", s); }
                    else { x += addDetailItem("Архітектура", 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("Час останнього запуску", date);
                }
                if(m.Domain){ x += addDetailItem((m.PartOfDomain ? "Домен" : "Робоча група"), EscapeHtml(m.Domain), s); }
                if(m.DomainState > 0){ x += addDetailItem("Стан домену", (domainStates[m.DomainState] ? domainStates[m.DomainState] : "Невідомо"), s); }
            }
            if (hardware.linux && hardware.linux.LastBootUpTime) {
                var lastBootUpTime = new Date(hardware.linux.LastBootUpTime);
                const date = printDateTime(lastBootUpTime);
                x += addDetailItem("Час останнього запуску", date);
            }
            if (hardware.linux && hardware.linux.arch) {
                var arch = hardware.linux.arch;
                x += addDetailItem("Архітектура", (['x86_64','amd64'].includes(arch) ? "64-bit" : ['i686','x86','i586'].includes(arch) ? "32-біта" : EscapeHtml(arch)), s);
            }
            if (hardware.linux && hardware.linux.kernel_release) { x += addDetailItem("Версія ядра", EscapeHtml(hardware.linux.kernel_release), s); }
            if (hardware.linux && hardware.linux.kernel_build) { x += addDetailItem("Збірка ядра", 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("Час останнього запуску", date);
            }
            if (x != '') { sections.push({ name: "Операційна система", 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("Mesh Agent", str);
                }
                if (node.firstconnect) { x += addDetailItem("Перше підключення агента", printDateTime(new Date(node.firstconnect))); }
                if ((node.conn & 1) != 0) {
                    x += addDetailItem("Востаннє агент було підключено", "Підключено зараз");
                } else {
                    if (node.lastconnect) { x += addDetailItem("Востаннє агент було підключено", printDateTime(new Date(node.lastconnect))); }
                }
                if (node.lastaddr) {
                    var splitip = node.lastaddr.split(':');
                    if (splitip.length > 2) {
                        // IPv6
                        x += addDetailItem("Остання IP-адреса агента", '<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("Остання IP-адреса агента", splitip[0]);
                        } else {
                            x += addDetailItem("Остання IP-адреса агента", '<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("Час компіляції", isNaN(d) ? EscapeHtml(hardware.agentvers.compileTime) : printDateTime(new Date(d)));
                    }
                }
                if (hardware.time != null) {
                    x += addDetailItem("Останні оновлення деталей", printDateTime(new Date(hardware.time)));
                }
                if (x != '') { sections.push({ name: "Mesh Agent", html: x, img: 'meshagent64.png' }); }
            }

            // Mobile
            if (hardware.mobile) {
                var x = '';
                if (hardware.mobile.brand && hardware.mobile.model) { x += addDetailItem("Модель", EscapeHtml(hardware.mobile.brand + ', ' + hardware.mobile.model), s); }
                if (hardware.mobile.device) { x += addDetailItem("Пристрій", EscapeHtml(hardware.mobile.device), s); }
                if (hardware.mobile.bootloader) { x += addDetailItem("Завантажувач", EscapeHtml(hardware.mobile.bootloader), s); }
                if (hardware.mobile.id) { x += addDetailItem("Ідентифікатор", EscapeHtml(hardware.mobile.id), s); }
                if (hardware.mobile.host) { x += addDetailItem("Ім'я хоста", EscapeHtml(hardware.mobile.host), s); }
                if (hardware.mobile.androidapi && hardware.mobile.androidrelease) { x += addDetailItem("Версія Android", EscapeHtml(hardware.mobile.androidrelease + ', API Level ' + hardware.mobile.androidapi), s); }
                if (x != '') { sections.push({ name: "Мобільний Пристрій", 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("Опис", EscapeHtml(m.desc).split('(R)').join('&reg;')); }
                    //if (m.dnssuffix) { x += addDetailItem("DNS Suffix", m.dnssuffix); }
                    if (m.mac) {
                        if (m.gatewaymac) {
                            x += addDetailItem("MAC рівень", format("MAC: {0}, шлюз: {1}", EscapeHtml(m.mac), EscapeHtml(m.gatewaymac)));
                        } else {
                            if (m.mac) { x += addDetailItem("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("IPv4 рівень", format("IP: {0}, Маска: {1}, Шлюз: {2}", EscapeHtml(ipv4layer.v4addr), EscapeHtml(ipv4layer.v4mask), EscapeHtml(ipv4layer.v4gateway)));
                        } else {
                            if (ipv4layer.v4addr) { x += addDetailItem("IPv4 рівень", format("IP: {0}", EscapeHtml(ipv4layer.v4addr))); }
                        }
                    }
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Мережа", 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("Опис", 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("MAC рівень", format("MAC: {0}, шлюз: {1}", EscapeHtml(m[0].mac), EscapeHtml(m[0].gatewaymac)));
                        } else {
                            x += addDetailItem("MAC рівень", format("MAC: {0}", EscapeHtml(m[0].mac)));
                        }
                    }
                    if (typeof m[0].speed == 'number' && (m[0].speed != 9223372036854775807 && m[0].speed > 0)) {
                        x += addDetailItem("Швидкість інтерфейсу", 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("Маска: {0}", EscapeHtml(iplayer.netmask))); }
                        if (iplayer.gateway) { items.push(format("Шлюз: {0}", EscapeHtml(iplayer.gateway))); }
                        if (items.length > 0) {
                            if (iplayer.family == 'IPv4') { x += addDetailItem("IPv4 рівень", items.join(", ")); }
                            if (iplayer.family == 'IPv6') { x += addDetailItem("IPv6 рівень", items.join(", ")); }
                        }
                    }
                    x += '</div></td></tr>';
                }
                if (hardware.network && hardware.network.dns) {
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += addDetailItem('<b>' + "DNS Сервери" + '</b>', EscapeHtml(hardware.network.dns.join(", ")));
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Мережа", html: x, img: 'networking64.png' }); }
            }

            // Attribute: Intel AMT
            if (node.intelamt != null) {
                var x = '';
                x += addDetailItem("Версія", (node.intelamt.ver) ? ('v' + EscapeHtml(node.intelamt.ver)) : ('<i>' + "Невідомо" + '</i>'), s);
                var provisioningStates = { 0: nobreak("Не Активовано"), 1: nobreak("Попередня активація"), 2: nobreak("Активовано") };
                var provisioningMode = '';
                if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Режим керування клієнтом (eng. CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Режим Керування Адміністратора (eng. ACM)"); } }
                x += addDetailItem("Стан Ініціалізації", ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('<i>' + "Невідомо" + '</i>')) + provisioningMode, s);
                x += addDetailItem("Безпека", (node.intelamt.tls == 1) ? "Захищено за допомогою TLS" : "TLS не встановлено", s);
                // Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
                x += addDetailItem("Облікові Дані Адміністратора", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0))) ? "Невідомо" : "Відоме як", s);
                if (x != '') {
                    if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
                        sections.push({ name: "Стандартне Управління Intel&reg; (Intel&reg; SM)", html: x, img: 'amt64.png' });
                    } else {
                        sections.push({ name: "Технологія Intel&reg; Active Management (Intel&reg; AMT)", html: x, img: 'amt64.png' });
                    }
                }
            }

            if (hardware.identifiers) {
                var x = '', ident = hardware.identifiers;
                // BIOS
                if (ident.bios_vendor) { x += addDetailItem("Постачальник", EscapeHtml(ident.bios_vendor), s); }
                if (ident.bios_version) { x += addDetailItem("Версія", EscapeHtml(ident.bios_version), s); }
                if (ident.bios_serial) { x += addDetailItem("Серійний номер", EscapeHtml(ident.bios_serial), s); }
                if (ident.bios_mode) { x += addDetailItem("Режим", EscapeHtml(ident.bios_mode), s); }
                if (x != '') { sections.push({ name: "BIOS", html: x, img: 'chip64.png' }); }

                // Motherboard
                x = '';
                if (ident.board_vendor) { x += addDetailItem("Постачальник", EscapeHtml(ident.board_vendor), s); }
                if (ident.board_name) { x += addDetailItem("Ім'я", EscapeHtml(ident.board_name), s); }
                if (ident.board_serial && (ident.board_serial != '')) { x += addDetailItem("Серійний номер", EscapeHtml(ident.board_serial), s); }
                if (ident.board_version) { x += addDetailItem("Версія", EscapeHtml(ident.board_version), s); }
                if (ident.product_uuid) { x += addDetailItem("Ідентифікатор", EscapeHtml(ident.product_uuid), s); }
                if (ident.cpu_name) { x += addDetailItem("ЦП", 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: "Материнська Плата", 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("Назва Продукту", EscapeHtml(ident.product_name), s); }
                if (ident.chassis_serial) { x += addDetailItem("Серійний номер", EscapeHtml(ident.chassis_serial), s); }
                if (ident.chassis_assettag) { x += addDetailItem("Тег активу", EscapeHtml(ident.chassis_assettag), s); }
                if (x != '') { sections.push({ name: "Система", html: x, img: 'system64.png' }); }
            }

            // TPM
            if (hardware.tpm) {
                var x = '', tpm = hardware.tpm;
                if (tpm.SpecVersion) { x += addDetailItem("СпецВерсія", parseFloat(EscapeHtml(tpm.SpecVersion)).toFixed(1), s); }
                if (tpm.ManufacturerId) { x += addDetailItem("Ідентифікатор", EscapeHtml(tpm.ManufacturerId), s); }
                if (tpm.ManufacturerVersion) { x += addDetailItem("Версія", EscapeHtml(tpm.ManufacturerVersion), s); }
                if (tpm.IsActivated != null) { x += addDetailItem("Активовано", (tpm.IsActivated ? "Yes" : "Ні"), s); }
                if (tpm.IsEnabled != null) { x += addDetailItem("Увімкнено", (tpm.IsEnabled ? "Yes" : "Ні"), s); }
                if (tpm.IsOwned != null) { x += addDetailItem("Owned", (tpm.IsOwned ? "Yes" : "Ні"), 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 : "Невідомо") + '</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} хвилин", 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 ? "Yes" : "Ні"), s); }
                    if (battery.DischargeRate) { x += addDetailItem("Discharge Rate", format("{0} mW", battery.DischargeRate), s); }
                    if (battery.Discharging != null) { x += addDetailItem("Discharging", (battery.Discharging ? "Yes" : "Ні"), 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("Здоров'я", format("{0} %", battery.Health), s); }
                    if (battery.BatteryCharge) { x += addDetailItem("Заряд батареї", 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("Об'єм / Швидкість", EscapeHtml(format("{0} Мб, {1} МГц", (m.Capacity / 1024 / 1024), m.Speed)), s); }
                        else if (m.Capacity) { x += addDetailItem("Об'єм", EscapeHtml(format("{0} Мб", (m.Capacity / 1024 / 1024))), s); }
                        if (m.PartNumber) { x += addDetailItem("Номер деталі (P/N)", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Пам'ять", 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("Об'єм / Швидкість", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Об'єм", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Номер деталі (P/N)", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Пам'ять", 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("Об'єм / Швидкість", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Об'єм", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Номер деталі (P/N)", EscapeHtml((m.Manufacturer && m.Manufacturer != '') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';
                    if (x != '') { sections.push({ name: "Пам'ять", 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("Модель", 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("Об'єм", format("{0} Мб", Math.floor(m.Size / 1024 / 1024)), s); }
                            if (typeof m.Size == 'string') { x += addDetailItem("Об'єм", 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("Стан", EscapeHtml(foundObject.Status), s);
                        }
                        x += '</div>';
                    }
                }
                x += '</table>';

                if (x != '') { sections.push({ name: "Сховище", 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: "Невідомо", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" };
                const conversionStatus = { 1: "Повністю Зашифрований", 2: "Виконується Шифрування", 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("Об'єм", 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("Вільне місце", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Змінний" : (m.cdrom == true ? "CD-ROM" : ''));
                        if (m.dType != null) { type = driveType.hasOwnProperty(m.dType) ? driveType[m.dType] : "Невідомо"; }
                        x += addDetailItem("Файлова система", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Невідомо" : 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 = "Заблоковано";
                            } else {
                                bitlockerState = (conversionStatus.hasOwnProperty(m.volumeStatus) ? conversionStatus[m.volumeStatus] : "Невідомо");
                            }
                        }
                        bitlockerState += (encMethod.hasOwnProperty(m.encryptionMethod) ? ' / ' + encMethod[m.encryptionMethod] : ' / ' + "Невідомо");
                        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: "Томи Сховища", 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("Об'єм", 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("Вільне місце", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Змінний" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("Файлова система", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Невідомо" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Томи Сховища", 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("Об'єм", EscapeHtml(m.size), s);
                    }
                    if (m.available) {
                        x += addDetailItem("Вільне місце", EscapeHtml(m.available), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Змінний" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("Файлова система", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Невідомо" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Томи Сховища", 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', "Немає інформації про цей пристрій.");
            } else {
                QH('p17info2', x);
            }
        }

        function deviceDetailsShowBitlockerInfo(drive, identifier, password) {
            if (xxdialogMode) return false;
            var x = '<div><p>' + "Ідентифікатор" + '</p><p style=user-select:text;font-weight:bold>' + (identifier ? decodeURIComponent(identifier) : "Невідомо") + '</p>';
            x += '<p>' + "Відновлення Пароля" + '</p><p style=user-select:text;font-weight:bold>' + (password ? decodeURIComponent(password) : "Невідомо") + '</p></div>';
            setModalContent('xxAddAgent', decodeURIComponent(drive) + ': ' + "Інформація о 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>' + "Завантаження настільних додатків..." + '</span>');
            QH('p18html', '<div style="text-align:center;padding:50px;color:gray">' + "Завантаження встановленого програмного забезпечення..." + '</div>');
            if ((currentNode.conn & 1) == 0) {
                QH('p18html', '<div style="text-align:center;padding:50px;color:#cc0000"><b>' + "Пристрій офлайн." + '</b></div>');
                QH('p18Status', '<span>' + "Офлайн" + '</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>' + "ПОМИЛКА:" + e.message + '</span>');
                installedAppsLoading = false;
                installedAppsLoadingType = 0;
            }
        }

        function loadStoreApps() {
            if (currentNode == null) return;
            if (installedAppsLoading) return;
            installedAppsLoading = true;
            installedAppsLoadingType = 2;
            QH('p18Status', '<span>' + "Завантаження додатків магазину..." + '</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> ' + "Настільні додатки" + ' (' + 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">' + "Ім'я" + '</th>';
            x += '<th style="text-align:left;width:120px">' + "Версія" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Видавець" + '</th>';
            x += '<th style="text-align:left;width:100px">' + "Дата встановлення" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Локація" + '</th>';
            x += '<th style="text-align:center;width:100px">' + "Дії" + '</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 ? "Немає відповідних додатків" + '"' + EscapeHtml(searchTerm) + '"' : "Настільні додатки не знайдено";
                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> ' + "Програми 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">' + "Ім'я" + '</th>';
				x += '<th style="text-align:left;width:120px">' + "Версія" + '</th>';
				x += '<th style="text-align:left;width:250px">' + "Пакет" + '</th>';
				x += '<th style="text-align:center;width:100px">' + "Область" + '</th>';
				x += '<th style="text-align:center;width:80px">' + "Дії" + '</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 ? "Немає програм магазину, що відповідають" + '"' + EscapeHtml(searchTerm) + '"' : "Програми магазину не знайдено";
					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="' + "Встановлено лише для поточного користувача" + '">' + "Користувач" + '</span>';
							} else if (app.scope.indexOf('System') >= 0) {
								scopeBadge = '<span class="badge bg-info" title="' + "Встановлено для всіх користувачів" + '">' + "Система" + '</span>';
							}
							if (app.scope.indexOf('Prov') >= 0) {
								scopeBadge += ' <span class="badge bg-secondary" title="' + "Підготовлено для нових користувачів" + '">' + "Підг" + '</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 = "Робочий стіл:" + ' ' + shownDesktop + '/' + totalDesktop;
            if (showStoreApps) {
                statusText += ' | ' + "Магазин:" + ' ' + 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>' + "ПОМИЛКА:" + "Команда видалення недоступна" + '</span>');
                return;
            }
            var dialogContent = '<div style="max-width:500px">';
            dialogContent += '<p><b>' + "Видалити це програмне забезпечення?" + '</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>' + "Буде виконано тихе видалення" + '</i></p>';
            dialogContent += '</div>';
            setModalContent('xxAddAgent', "Видалити програмне забезпечення", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
                QH('p18Status', '<span>' + "Видалення..." + '</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', "Видалення розпочато. Натисніть Оновити для оновлення"); }, 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>' + "Видалити цей додаток магазину?" + '</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', "Видалити додаток магазину", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
				QH('p18Status', '<span>' + "Видалення..." + '</span>');
				meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'uninstallstoreapp', value: '"' + cmd.replace(/"/g, '\\"') + '"' });
				setTimeout(function() { QH('p18Status', "Команду видалення надіслано. Натисніть Оновити для оновлення"); }, 2000);
			});
		}

        function handleUninstallResponse(data) {
            try {
                var result = JSON.parse(data);
                if (result.success) {
                    QH('p18Status', '<span>✓ ' + "Успіх" + '</span> ' + "Натисніть Оновити для оновлення");
                } else if (result.error) {
                    QH('p18Status', '<span>✗ ' + "ПОМИЛКА:" + 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', ' - ' + "Консоль Мого сервера");
                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) ? "Агент онлайн" : "Агент офлайн"
                    if ((consoleNode.conn & 16) != 0) { onlineText += ", MQTT працює" }
                    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', "Доступ Відмовлено");
                    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>' + "НАТИСНІТЬ" + '&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>' + "Відправити ядро ​​сервера за умовчанням" + '</option>' +
                    '<option value=2>' + "Очистити ядро" + '</option>' +
                    '<option value=3>' + "Відправити файл ядра" + '</option>' +
                    '<option value=4>' + "М'яке відключення агенту" + '</option>' +
                    '<option value=5>' + "Жорстке відключення агента" + '</option>' +
                    '<option value=6>' + "Відправити ядро для ​​відновлення" + '</option>' +
                    '<option value=7>' + "Відправити спрощене ядро" + '</option>' +
                    '<option value=8>' + "Restart agent service" + '</option>' +
                    '<option value=9>' + "Примусове оновити агента" + '</option></select>';
                setModalContent('xxAddAgent', "Виконати дію агента", addHtmlFormFloating("Дія", 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', "Завантажити ядро ​​Mesh Agent", '');
            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', "Попередні спроби входу", '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', "Керувати Світлиною Акаунту", 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>' + "Верифіковано номер телефону" + '</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() />' + "Видалити номер телефону" + '</label></div>';
                setModalContent('xxAddAgent', "Телефонні Сповіщення", 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>' + "Введіть свій номер телефону для повідомлень SMS. Після верифікації цей номер можна використовувати для перевірки входу та інших повідомлень.";
                x += '<br /><br /><div style=width:100%;text-align:center>' + "Номер телефону:" + ' <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', "Телефонні Сповіщення", 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>' + "Вхід підтверджено" + '</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() />' + "Видалити підтримку месенджера" + '</label></div>';
                setModalContent('xxAddAgent', "Сповіщення месенджера", 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>' + "Введіть свою службу обміну повідомленнями. Після верифікації цей сервер зможе надсилати підтвердження входу та інші сповіщення." + '<br /><br />';
                var y = '<select id=d2serviceselect style=width:160px;margin-left:8px onchange=account_manageMessagingValidate()>';
                if ((serverinfo.userMsgProviders & 1) != 0) { y += '<option value=1>' + "Telegram" + '</option>'; }
                if ((serverinfo.userMsgProviders & 4) != 0) { y += '<option value=4>' + "Discord" + '</option>'; }
                if ((serverinfo.userMsgProviders & 8) != 0) { y += '<option value=8>' + "XMPP" + '</option>'; }
                if ((serverinfo.userMsgProviders & 16) != 0) { y += '<option value=16>' + "CallMeBot" + '</option>'; }
                if ((serverinfo.userMsgProviders & 32) != 0) { y += '<option value=32>' + "Pushover" + '</option>'; }
                if ((serverinfo.userMsgProviders & 64) != 0) { y += '<option value=64>' + "ntfy" + '</option>'; }
                if ((serverinfo.userMsgProviders & 128) != 0) { y += '<option value=128>' + "Zulip" + '</option>'; }
                if ((serverinfo.userMsgProviders & 256) != 0) { y += '<option value=256>' + "Slack" + '</option>'; }
                y += '</select>';
                x += '<table><tr><td>' + "Сервіс" + '<td>' + y;
                x += '<tr><td>' + "Обробка" + '<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">' + "японська" + '</a></div>'; }
                x += '<div id=d2callmebotinfo style=display:none><br /><a href=https://www.callmebot.com/blog/free-api-signal-send-messages/ target="_callmebot">' + "Signal" + '</a>, <a href=https://www.callmebot.com/blog/free-api-whatsapp-messages/ target="_callmebot">' + "WhatsApp" + '</a>, <a href=https://www.callmebot.com/blog/free-api-facebook-messenger/ target="_callmebot">' + "Facebook" + '</a>, <a href=https://www.callmebot.com/blog/telegram-text-messages/ target="_callmebot">' + "Telegram" + '</a></div>';
                x += '<div id=d2pushoverinfo style=display:none><br /><a href=https://pushover.net/ target="_pushover">' + "Інформація про Pushover.net" + '</a></div>';
                x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Безкоштовний сервіс від ntfy.sh" + '</a></div>';
                x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Інсталяція Slack Webhook" + '</a></div>';
                xxdialogTag = 'VerifyMessaging';
                setModalContent('xxAddAgent', "Сповіщення месенджера", 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'] = "Username:0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "ім'я_користувача@server.ua"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Ключ користувача"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Тема"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "ім'я_користувача@server.ua"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Ім'я користувача"; }
            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', "Автентифікація е-пошти", "Якщо увімкнено, то під час кожного входу ви матимете можливість отримати токен входу для вашого акаунту електронною поштою для додаткової безпеки." + '<br /><br /><label><input id=email2facheck type=checkbox class="form-check-input me-2" ' + (emailU2Fenabled ? 'checked' : '') + '/>' + "Увімкнути двофакторну автентифікацію е-поштою." + '</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', "Застосунок Автентифікатора", ('<div id=d2optinfo>' + "Завантаження..." + '</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', "Застосунок Автентифікатора", "Підтвердити видалення двофакторної автентифікації через застосунок?");
            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', "Пристрій Автентифікації", "Підтвердити видалення пристрою 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', "Пристрій Автентифікації", '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 = "Виберіть пристрій для реєстрації та автентифікації push-сповіщень. Після вибору пристрій запитає підтвердження." + '<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("Пристрій", y);
                    setModalContent('xxAddAgent', "Пристрій Автентифікації", 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 = "Введіть назву ключа, який ви бажаєте додати" + '<br /><br />';
                x += addHtmlFormFloating("Назва Ключа", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "МійКлюч" + '" onkeyup=account_addhkeyValidate(event,2) />');
            } else if (type == 2) {
                var x = "Введіть назву ключа, виберіть поле OTP і натисніть кнопку на YubiKey&trade;." + '<br /><br />';
                x += addHtmlFormFloating("Назва Ключа", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "МійКлюч" + '" 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', "Додати Ключ Безпеки", 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', "Додати Ключ Безпеки", '<br />' + "Перевірка..." + '<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': "Африканська", 'sq': "Албанська", 'ar': "Арабська (Стандартна)", 'ar-dz': "Арабська (Алжир)", 'ar-bh': "Арабська (Бахрейн)", 'ar-eg': "Арабська (Єгипет)", 'ar-iq': "Арабська (Ірак)", 'ar-jo': "Арабська (Йорданія)", 'ar-kw': "Арабська (Кувейт)", 'ar-lb': "Арабська (Ліван)", 'ar-ly': "Арабська (Лівія)", 'ar-ma': "Арабська (Марокко)", 'ar-om': "Арабська (Оман)", 'ar-qa': "Арабська (Катар)", 'ar-sa': "Арабська (Саудівська Аравія)", 'ar-sy': "Арабська (Сирія)", 'ar-tn': "Арабська (Туніс)", 'ar-ae': "Арабська (ОАЕ)", 'ar-ye': "Арабська (Ємен)", 'an': "Арагонська", 'hy': "Вірменська", 'as': "Асамська", 'ast': "Астурійська", 'az': "Азербайджанська", 'eu': "Баскська", 'bg': "Болгарська", 'be': "Білоруська", 'bn': "Бенгальська", 'bs': "Боснійська", 'br': "Бретонська", 'my': "Бірманська", 'ca': "Каталанська", 'ch': "Чаморро", 'ce': "Чеченська", 'zh': "Китайська", 'zh-hk': "Китайська (Гонконг)", 'zh-cn': "Китайська (КНР)", 'zh-sg': "Китайська (Сінгапур)", 'zh-tw': "Китайська (Тайвань)", 'cv': "Чуваська", 'co': "Корсиканська", 'cr': "Крі", 'hr': "Хорватська", 'cs': "Чеська", 'da': "Данська", 'nl': "Голландська (Стандартна)", 'nl-be': "Голландська (Бельгійська)", 'en': "Англійська", 'en-au': "Англійська (Австралія)", 'en-bz': "Англійська (Беліз)", 'en-ca': "Англійська (Канада)", 'en-ie': "Англійська (Ірландія)", 'en-jm': "Англійська (Ямайка)", 'en-nz': "Англійська (Нова Зеландія)", 'en-ph': "Англійська (Філіппіни)", 'en-za': "Англійська (Південна Африка)", 'en-tt': "Англійська (Трінідад і Тобаго)", 'en-gb': "Англійська (Великобританія)", 'en-us': "Англійська (Сполучені Штати)", 'en-zw': "Англійська (Зімбабве)", 'eo': "Есперанто", 'et': "Естонська", 'fo': "Фарерська", 'fa': "Фарсі (Перська)", 'fj': "Фіджійська", 'fi': "Фінська", 'fr': "Французька (Стандартна)", 'fr-be': "Французька (Бельгія)", 'fr-ca': "Французька (Канада)", 'fr-fr': "Французька (Франція)", 'fr-lu': "Французька (Люксембург)", 'fr-mc': "Французька (Монако)", 'fr-ch': "Французька (Швейцарія)", 'fy': "Фризька", 'fur': "Фріульська", 'gd': "Ґельська (Шотландська)", 'gd-ie': "Ґельська (Ірландська)", 'gl': "Galacian", 'ka': "Грузинська", 'de': "Німецька (Cтандартна)", 'de-at': "Німецька (Австрія)", 'de-de': "Німецька (Німеччина)", 'de-li': "Німецька (Ліхтенштейн)", 'de-lu': "Німецька (Люксембург)", 'de-ch': "Німецька (Швейцарія)", 'el': "Грецька", 'gu': "Гуджараті", 'ht': "Гаїтянська", 'he': "Іврит", 'hi': "Хінді", 'hu': "Мадярська", 'is': "Ісландська", 'id': "Індонезійська", 'iu': "Інуктітут", 'ga': "Ірландська", 'it': "Італійська (стандартна)", 'it-ch': "Італійська (Швейцарія)", 'ja': "Японська", 'kn': "Каннада", 'ks': "Кашмір", 'kk': "Казахська", 'km': "Кмерська", 'ky': "Киргизька", 'tlh': "Клінгонська", 'ko': "Корейська", 'ko-kp': "Корейська (Північна Корея)", 'ko-kr': "Корейська (Південна Корея)", 'la': "Латиниця", 'lv': "Латиська", 'lt': "Литовська", 'lb': "Люксембурзька", 'mk': "Македонська (КЮРМ)", 'ms': "Малайська", 'ml': "Малаялам", 'mt': "Мальтійська", 'mi': "Маорі", 'mr': "Маратхі", 'mo': "Молдавська", 'nv': "Навахо", 'ng': "Ндонга", 'ne': "Непальська", 'no': "Норвезька", 'nb': "Норвезька (Букмал)", 'nn': "Норвезька (Нюнорск)", 'oc': "Окситанська", 'or': "Орія", 'om': "Оромо", 'fa-ir': "Перська/Іранська", 'pl': "Польська", 'pt': "Португальська", 'pt-br': "Португальська (Бразилія)", 'pa': "Пенджабі", 'pa-in': "Пенджабі (Індія)", 'pa-pk': "Пенджабі (Пакистан)", 'qu': "Кечуа", 'rm': "Ретороманська", 'ro': "Румунська", 'ro-mo': "Румунська (Молдова)", 'ru': "московитська", 'ru-mo': "московитська (Молдова)", 'sz': "Самі (Самі)", 'sg': "Санго", 'sa': "Санскрит", 'sc': "Сардинська", 'sd': "Сіндхі", 'si': "Сінгальська", 'sr': "Сербська", 'sk': "Словацька", 'sl': "Словенська", 'so': "Сомані", 'sb': "Лужицька", 'es': "Іспанська", 'es-ar': "Іспанська (Аргентина)", 'es-bo': "Іспанська (Болівія)", 'es-cl': "Іспанська (Чилі)", 'es-co': "Іспанська (Колумбія)", 'es-cr': "Іспанська (Коста-Ріка)", 'es-do': "Іспанська (Домініканська Республіка)", 'es-ec': "Іспанська (Еквадор)", 'es-sv': "Іспанська (Сальвадор)", 'es-gt': "Іспанська (Гватемала)", 'es-hn': "Іспанська (Гондурас)", 'es-mx': "Іспанська (Мексика)", 'es-ni': "Іспанська (Нікарагуа)", 'es-pa': "Іспанська (Панама)", 'es-py': "Іспанська (Парагвай)", 'es-pe': "Іспанська (Перу)", 'es-pr': "Іспанська (Пуерто-Ріко)", 'es-es': "Іспанська (Іспанія)", 'es-uy': "Іспанська (Уругвай)", 'es-ve': "Іспанська (Венесуела)", 'sx': "Суту", 'sw': "Суахілі", 'sv': "Шведська", 'sv-fi': "Шведська (Фінляндія)", 'sv-sv': "Шведська (Швеція)", 'ta': "Тамільська", 'tt': "Татарська", 'te': "Телуга", 'th': "Тайський", 'tig': "Тигре", 'ts': "Тсонга", 'tn': "Тсвана", 'tr': "Турецька", 'tk': "Туркменська", 'uk': "Українська", 'hsb': "Верхньолужицька", 'ur': "Урду", 've': "Венда", 'vi': "В'єтнамська", 'vo': "Волапюк", 'wa': "Walloon / Валлонська", 'cy': "Валлійська", 'xh': "Кхоса", 'ji': "Їдиш", 'zu': "Zulu" };
        var loclistex = { 'zh-chs': "Китайська (Спрощена)", 'zh-cht': "Китайська (Традиційна)" };
        function account_showLocalizationSettings() {
            if (xxdialogMode) return false;
            var n = getstore('loctag', 0), y = '';
            var x = '<select id=d2locselect class="form-select"><option value="*">' + "Використати налаштування браузера" + '</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 += "Зміна мови потребуватиме оновлення сторінки." + '<br /><br />';
                var z = '<select id=d2langselect class="form-select"><option value="*">' + "Використати налаштування браузера" + '</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("Мова", z);
            }
            y += addHtmlFormFloating("Дата та Час", x);

            if ((userinfo.siteadmin == 0xFFFFFFFF) && (domain == '')) {
                y += '<br /><a rel="noreferrer noopener" target="_blank" href="translator.htm">' + "Допомогти перекласти MeshCentral" + '</a>';
            }

            setModalContent('xxAddAgent', "Параметри Локалізації", 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 = "Створіть тимчасове ім'я користувача та пароль, які можна використовувати як альтернативний лоґін для входу до вашого акаунту. Це корисно, щоб надати засобам або іншим службам доступ до вашого акаунту." + '<br /><br />';
            var options = { 0: "Необмежено", 1: "1 хвилина", 5: "5 хвилин", 10: "10 хвилин", 15: "15 хвилин", 30: "30 хвилин", 45: "45 хвилин", 60: "60 хвилин", 120: "2 години", 240: "4 години", 480: "8 годин", 720: "12 годин", 960: "16 годин", 1440: "24 години", 2880: "2 дні", 5760: "4 дні", 10080: "7 днів" }
            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="' + "Ім'я Токена" + '" onchange=account_createLoginTokenValidate() onkeyup=account_createLoginTokenValidate()>';
            x += '<label for=d2tokenName>' + "Ім'я Токена" + '</label>';
            x += '</div>';
            x += addHtmlFormFloating("Термін придатності", '<select class="boxsize form-select" id=d2tokenExpire>' + y + '</select>')

            setModalContent('xxAddAgent', "Створити Токен Входу", 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 />' + "Звук сповіщення" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyGroupName class="form-check-input me-2" type=checkbox />' + "Показувати назву групи пристроїв" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Підключення пристрою" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Відключення пристрою" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Події Intel&reg; AMT для стільниці та послідовного порту" + '</label></div>';

            setModalContent('xxAddAgent', "Налаштування Сповіщень", 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 = "Натисніть OK, щоб надіслати електронний лист підтвердження на:" + '<br /><div style=padding:8px><b>' + EscapeHtml(userinfo.email) + '</b></div>' + "Будь ласка, зачекайте трохи часу, поки буде отримано підтвердження.";
            setModalContent('xxAddAgent', "Верифікація е-пошти", 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 = "Змініть адресу е-пошти свого акаунту тут." + '<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', "Змінити Адресу е-пошти", 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 = "Щоб видалити цей акаунт, введіть пароль акаунту в обидва поля нижче та натисніть \"Ok\"." + '<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', "Видалити Акаунт", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                dialogclose(1);
            });

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

        function account_showChangePassword() {
            if (xxdialogMode) return false;
            var x = "Змініть пароль свого акаунту, ввівши старий пароль та двічі новий пароль в поля нижче.";
            if (features & 0x00010000) { " Підказку для пароля можна використати, але не рекомендується."; }
            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>' + "Вимоги:" + 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', "Змінити Пароль", 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: "Типовий" },
                { 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', "Змінити тему", 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', "Нова Група Пристроїв", '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', "Безпека акаунту", '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', "Безпека акаунту", '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 = "Створіть нову групу пристроїв за допомогою параметрів нижче." + '<br /><br />';
            var localGroupType = '';

            x += addHtmlFormFloating("Ім'я", '<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>' + "Локальні пристрої, без агента" + '</option>'; }
            if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=103>' + "Пристрої без агента, ретрансльовані агентом" + '</option>'; }
            if (features2 & 0x10000) {
                if ((features & 1) == 0) { localGroupType += '<option value=4>' + "IP-KVM / Електроживлення" + '</option>'; }
                if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=104>' + "IP-KVM / Керування живленням ретранслюється через агента" + '</option>'; }
            }
            x += addHtmlFormFloating("Тип", '<select id=dp2meshtype class=form-select onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=2>' + "Керування програмним агентом" + '</option><option value=1>' + "Лише Intel&reg; AMT, без агента" + '</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("Пристрій Ретрансляції", '<select id=d2devrelay onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select >' + relayDevices2.join('') + '</select>');
                x += '</div>';
            }
            x += addHtmlFormFloating("Опис", '<textarea id=dp2meshdesc maxlength=1024 class=form-control></textarea>');
            x += '<div id=d2ipkvm style=display:none><hr />';
            x += addHtmlFormFloating("Модель", '<select id=dp2ipkvmmodel onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select ><option value=1>' + "Raritan Dominion KX III" + '</option><option value=2>' + "Web Power Switch 7" + '</option></select>');
            x += addHtmlFormFloating("Ім'я хоста", '<input id=dp2ipkvmhost maxlength=128 placeholder="Hostname" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Ім'я користувача", '<input id=dp2ipkvmuser maxlength=128 placeholder="Username" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Пароль", '<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', "Нова Група Пристроїв", x);

            // Show the modal
            showModal('xxAddAgentModal', 'idx_dlgOkButton', account_createMeshEx);

            // Validate the mesh create form
            account_validateMeshCreate();
            document.getElementById('dp2meshname').focus();
            return false;
        }

        function account_validateMeshCreate(e, x) {
            var meshtype = parseInt(Q('dp2meshtype').value);
            if ((x == 1) && (e != null) && (e.key == "Enter") && (Q('dp2meshname').value.length > 0)) { Q('dp2meshtype').focus(); }
            if ((x == 2) && (e != null) && (e.key == "Enter")) { Q('dp2meshdesc').focus(); }
            var ok = (Q('dp2meshname').value.length > 0);
            QV('d2ipkvm', (meshtype == 4) || (meshtype == 104));
            try { QV('d2devrelaydiv', meshtype > 100); } catch (ex) { }
            if (meshtype == 4) { if ((Q('dp2ipkvmhost').value.length == 0) && (Q('dp2ipkvmuser').value.length == 0) && (Q('dp2ipkvmpass').value.length == 0)) { ok = false; } }
            QE('idx_dlgOkButton', ok);
        }

        function account_createMeshEx(button, tag) {
            var meshtype = parseInt(Q('dp2meshtype').value);
            var cmd = { action: 'createmesh', meshname: Q('dp2meshname').value, meshtype: meshtype, desc: Q('dp2meshdesc').value };
            if ((meshtype == 4) || (meshtype == 104)) {
                cmd.kvmmodel = parseInt(Q('dp2ipkvmmodel').value);
                cmd.kvmhost = Q('dp2ipkvmhost').value;
                cmd.kvmuser = Q('dp2ipkvmuser').value;
                cmd.kvmpass = Q('dp2ipkvmpass').value;
            }
            if (meshtype > 100) {
                cmd.meshtype = (meshtype - 100);
                cmd.relayid = Q('d2devrelay').value;
            }
            meshserver.send(cmd);
        }

        function account_validateDeleteAccount() {
            QE('account_dlgOkButton', (Q('apassword1').value.length > 0) && (Q('apassword1').value == Q('apassword2').value));
        }

        function account_validateNewPassword() {
            var r = '', ok = (Q('apassword0').value.length > 0) && (Q('apassword1').value.length > 0) && (Q('apassword1').value == Q('apassword2').value) && (Q('apassword0').value != Q('apassword1').value);
            if ((features & 0x00010000) && (Q('apasswordhint').value == Q('apassword1').value)) { ok = false; }
            if (Q('apassword1').value != '') {
                if (passRequirements == null || passRequirements == '') {
                    // No password requirements, display password strength
                    var passStrength = checkPasswordStrength(Q('apassword1').value);
                    if (passStrength >= 80) { r = '<span style=color:green>' + "Надійний" + '<span>'; } else if (passStrength >= 60) { r = '<span style=color:blue>' + "Добре" + '<span>'; } else { r = '<span style=color:red>' + "Хиткий" + '<span>'; }
                } else {
                    // Password requirements provided, use that
                    var passReq = checkPasswordRequirements(Q('apassword1').value, passRequirements);
                    if (passReq == false) { ok = false; r = '<span style=color:red>' + "Поліс" + '<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 = "Часткові Дозволи";
                if (meshrights == 0xFFFFFFFF) rights = "Повноправний Адміністратор"; else if (meshrights == 0) rights = "Немає Дозволів";

                // 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>' + "Активні Токени Входу" + '</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> ' + "Новий" + '</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>' + "Ім'я" + '</th><th scope=col style=text-align:left>' + "Ім'я користувача" + '</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="' + "Видалити токен входу" + '" role=button><i class="fa-solid fa-trash text-danger fa-xs"></i></a>';
                    var details = '';
                    if (ltoken.expire != 0) { details = EscapeHtml(format("Застаріє {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 = "Підтвердити видалення цього токена входу?" + '<br /><br />';
            x += addHtmlValue("Ім'я", EscapeHtml(token.name));
            x += addHtmlValue("Ім'я користувача", EscapeHtml(token.tokenUser));
            setModalContent('xxAddAgent', "Видалити токен входу", 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>' + "Налаштувати цей сервер на автоматичне завантаження резервних копій на Google Drive. Почніть із створення та введення ClientID і ClientSecret Google Drive для власного акаунту." + '</div><br />';
                x += addHtmlValue("Облікові Дані", '<a href="https://console.developers.google.com/apis/api/drive.googleapis.com/credentials" rel="noreferrer noopener" target="_blank">' + "Консоль Google Drive" + '</a>');
                x += addHtmlFormFloating("ID Клієнта", '<input id=gdclientid class="form-control" placeholder="xxxxxxxx.apps.googleusercontent.com" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                x += addHtmlFormFloating("Секретний Ключ Клієнта", '<input id=gdclientsecret class="form-control" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                setModalContent('xxAddAgent', "Резервне копіювання до 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>' + "Резервне копіювання Google Drive активне." + '</div>';
                x += '<br /><label><input id=gdcheck type=checkbox class="form-check-input me-2" onchange=server_setupGoogleDriveBackupCheck2() />' + "Видалити Конфігурацію" + '</label>';
                setModalContent('xxAddAgent', "Резервне копіювання до 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 = "Введіть необов'язковий пароль для резервної копії" + '<br /><br />';
            x += '<tr><td align=right>' + nobreak("Пароль: ") + '</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') + ' /> ' + "Використовувати системний пароль" + '</label></div>';
            setModalContent('xxAddAgent', "Завантажте Резервну Копію Сервера", 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 = "Відновити сервер за допомогою резервної копії," + ' <span style=color:red>' + "що призведе до видалення існуючих даних сервера." + '</span> ' + "Запускайте це, лише якщо знаєте, що робите." + '<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("Пароль резервної копії: ") + '</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') + ' /> ' + "Використовувати системний пароль" + '</label></div>';
            setModalContent('xxAddAgent', "Відновити сервер", 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', "Відновити сервер", "Завантаження резервної копії");
            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', "Відновити сервер", "Помилка мережі: " + response.status);
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        QE('idx_dlgOkButton', true);
                    }
                })
                .catch((error) => {
                    setModalContent('xxAddAgent', "Відновити сервер", "Помилка: " + 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', "Версія MeshCentral", "Завантаження...");
            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 != "Невідомо") && (xxdialogTag.current != xxdialogTag.stable) && (latestCheck == false)));
            QE('d2updateCheck2', ((xxdialogTag.latest != "Невідомо") && (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', "Помилки Сервера", "Завантаження...");
            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>' + "Немає" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span role=button tabindex=0 title="' + "Клікніть тут, щоб змінити назву групи пристроїв" + '" onclick=p20editmesh(1) onkeyup="if (event.key == \'Enter\') p20editmesh(1)">' + mname + ' <i class="fa-solid fa-pencil fa-2xs"></i></span>'; }
            QH('p20meshName', mname);
            QV('MeshSummary', (currentMesh.mtype != 4));

            var meshtype = format("Невідоме #{0}", currentMesh.mtype);
            if (currentMesh.mtype == 1) meshtype = "Лише Intel&reg; AMT, без агента";
            if (currentMesh.mtype == 2) meshtype = "Керується програмним агентом";
            if (currentMesh.mtype == 3) { if (currentMesh.relayid == null) { meshtype = "Локальні пристрої, без агента"; } else { meshtype = "Пристрої без агента, ретрансльовані агентом"; } }
            if (currentMesh.mtype == 4) { if (currentMesh.relayid == null) { meshtype = "Пристрій IP-KVM"; } else { meshtype = "Пристрій IP-KVM ретранслюється через агента"; } if (currentMesh.kvm.model == 1) { meshtype += ', ' + 'Raritan KX III'; } }

            var x = '';
            if ((args.hide & 8) != 0) { x += addHtmlValue("Ім'я", mname); } // If title bar is hidden, display the mesh name here
            x += addHtmlValue("Опис", addLinkConditional(((currentMesh.desc && currentMesh.desc != '') ? EscapeHtml(currentMesh.desc) : ('<i>' + "Немає" + '</i>')), 'p20editmesh(2)', (meshrights & 1) != 0));

            // Display group type
            x += addHtmlValue("Тип", 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>' + "Невідомо" + '</i>';
                var relayNode = getNodeFromId(currentMesh.relayid);
                if (relayNode != null) { relayName = EscapeHtml(relayNode.name) + ' (' + EscapeHtml(relayNode.meshnamel) + ')'; }
                x += addHtmlValue("Пристрій Ретрансляції", addLinkConditional(relayName, 'p20editmeshrelay()', (meshrights & 1) != 0));
            }

            // Display IP-KVM information if needed
            if (currentMesh.mtype == 4) {
                x += addHtmlValue("Ім'я хоста", currentMesh.kvm.host);
                x += addHtmlValue("Ім'я користувача", currentMesh.kvm.user);
            }

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

            // Display creation time
            if (currentMesh.creation != null) { x += addHtmlValue("Час Створення", printDateTime(new Date(currentMesh.creation))); }

            // Display features
            if (currentMesh.mtype != 3) {
                var meshFeatures = [];
                if (currentMesh.flags) {
                    if (currentMesh.flags & 1) { meshFeatures.push("Авто-Видалення"); }
                    if (currentMesh.flags & 2) { meshFeatures.push((currentMesh.mtype == 4) ? "Синхронізація імен портів" : "Синхронізація Імені Хоста"); }
                    if (currentMesh.flags & 8) { meshFeatures.push("надавати перевагу --agentname"); }
                    if (currentMesh.flags & 16) { meshFeatures.push("дозволити перевизначення"); }
                    if ((serverinfo.devGroupSessionRecording == 1) && (currentMesh.flags & 4)) { meshFeatures.push("Запис Сеансів"); }
                }
                if ((typeof currentMesh.expireDevs == 'number') && (currentMesh.expireDevs > 0)) { meshFeatures.push("Видалити неактивний"); }
                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Немає" + '</i>'; }
                x += addHtmlValue("Можливості", 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("Запит до Стільниці та Панелі Засобів"); } else if (consent & 0x0040) { meshFeatures.push("Стільниця: панель підключення"); } else if (consent & 0x0008) { meshFeatures.push("Стільниця: запит"); } else { if (consent & 0x0001) { meshFeatures.push("Стільниця: сповістити"); } }
                if (consent & 0x0010) { meshFeatures.push("Термінал: запит"); } else { if (consent & 0x0002) { meshFeatures.push("Термінал: сповістити"); } }
                if (consent & 0x0020) { meshFeatures.push("Файли: запит"); } else { if (consent & 0x0004) { meshFeatures.push("Файли: сповістити"); } }
                if (consent == 7) { meshFeatures = ["Завжди сповіщати"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Завжди запитувати"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Немає" + '</i>'; }
                x += addHtmlValue("Згода користувача", 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("Підключитись"); }
                    if (meshNotify & 4) { meshNotifyStr.push("Відключити"); }
                    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("е-пошта ({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("Обмін повідомленнями ({0})", xx)); }
                    }
                    if (meshNotifyStr.length == 0) { meshNotifyStr.push('<i>' + "Немає" + '</i>'); }
                    x += addHtmlValue("Сповіщення", addLink(meshNotifyStr.join(', '), 'p20editMeshNotify()'));
                }
            }

            // Display invitation codes
            if ((features & 0x01000000) && (currentMesh.mtype == 2)) {
                var inviteCodeStr = '<i>' + "Немає" + '</i>', icodes = false;
                if (currentMesh.invite != null) { icodes = true; inviteCodeStr = currentMesh.invite.codes.join(', '); /* + ', ' + currentMesh.invite.flags;*/ }
                x += addHtmlValue("Коди запрошень", 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 = "Політика відсутня";
                if (currentMesh.amt) {
                    if (currentMesh.amt.type == 1) { intelAmtPolicy = "Деактивувати"; }
                    else if (currentMesh.amt.type == 2) {
                        intelAmtPolicy = "Простий Client Control Mode (CCM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 3) {
                        intelAmtPolicy = "Простий Admin Control Mode (ACM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 4) {
                        intelAmtPolicy = "Повністю Автоматичний";
                    }
                }
                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=' + "Примітки" + ' title="' + "Див. примітки щодо цієї групи пристроїв" + '" 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> ' + "Додати Користувачів" + '</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> ' + "Додати Групу Користувачів" + '</button>'; }
                }
            }

            //if (meshrights & 4) { }
            if (currentMesh.mtype == 1) {
                if (meshrights & 1) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" title="' + "Імпортувати пристрої Intel&reg; AMT." + '" onclick=\'return showAmtImport("' + currentMesh._id + '")\'><i class="fa-solid fa-circle-arrow-down"></i> ' + "Імпорт" + '</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="' + "Виконати активацію та налаштування Intel&reg; AMT." + '" onclick=\'return showAmtSetup("' + currentMesh._id + '")\'><i class="fa-solid fa-gear"></i> ' + "Налаштувати" + '</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="' + "Додати новий комп'ютер до цієї групи пристроїв, встановивши MeshAgent." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Додати агент" + '</button>';
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick=\'return inviteAgentToMesh("' + currentMesh._id + '")\' title="' + "Запросіть когось інсталювати MeshAgent у цю групу пристроїв." + '"><i class="fa-solid fa-user-plus"></i> ' + "Запросити" + '</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="' + "Додати пристрій, що знаходиться в локальній мережі." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Додати Пристрій" + '</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="' + "Перемикач Intel AMT у режимі Admin Control Mode (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>' + "Авторизація користувача" + '</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="' + "Видалити права користувача для цієї групи пристроїв" + '"><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="' + "Користувач" + '" 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>' + "Спільний доступ до пристрою активний" + '</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="' + "Посилання для Поширення Пристрою" + '" 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="' + "Видалити поширення до пристрою" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                    var type = ''; if (dshare.p <= 7) { type = ['', "Термінал", "Стільниця", "Стільниця + Термінал", "Файли", "Термінал + Файли", "Стільниця + Файли", "Стільниця + Термінал + Файли"][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} виконати {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} через {2} хвилини", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        } else {
                            details = format("{0}, {1} через {2} хвилин", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        }
                    }
                    if (dshare.recurring == 1) { details += ", Повторюється щодня"; }
                    if (dshare.recurring == 2) { details += ", Повторюється щотижня"; }
                    if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Лише перегляд робочого столу"; }
                    if (dshare.consent != null) {
                        if (dshare.consent == 0) { details += ", Нема Погодження"; } else {
                            if ((dshare.consent & 0x0038) != 0) { details += ", Запит на погодження"; }
                            if ((dshare.consent & 0x0040) != 0) { details += ", Панель Засобів"; }
                        }
                    }
                    var guestName = EscapeHtml(dshare.guestName);
                    if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Само-Поширення Агента" + '</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="' + "Отримати перелік пристроїв" + '" src="images/link4.png" /></a>';
                if ((features & 1) == 0) { dllist += ' <a onclick=meshImportDeviceList()><img title="' + "Імпортувати перелік пристроїв" + '" 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>' + "Пристрої" + '</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>' + "Немає" + '</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>' + "Видалити Групу" + '</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 = "Імпортувати Перелік локальних пристроїв Intel&reg; AMT у форматі MeshCommander JSON." + '<br /><br /><input type=file id=d4importFile class="form-control" accept=".json" onchange=meshImportDeviceListValidate() />';
            setModalContent('xxAddAgent', "Імпортувати пристрої 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', "Імпортувати пристрої Intel&reg; AMT", format("Хибний файл JSON: {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', "Імпортувати Акаунт Користувача", 'Unable to import any device.');
                    }
                } else {
                    setModalContent('xxAddAgent', "Імпортувати пристрої 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>' + "Простий Admin Control Mode (ACM)" + '</option>'; }
            x += addHtmlFormFloating("Тип", '<select id=dp20amtpolicy class="form-select" onchange=p20editMeshAmtChange()><option value=0>' + "Політика відсутня" + '</option><option value=1>' + "Деактивувати" + '</option><option value=2>' + "Простий Client Control Mode (CCM)" + '</option>' + acmoption + '<option value=4>' + "Повністю Автоматичний" + '</option></select>')
            x += '<div id=dp20amtpolicydiv></div>';

            setModalContent('xxAddAgent', "Полісі 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>' + "Зберегти існуючий пароль" + '</option>' : '';
                x += addHtmlFormFloating("Пароль", '<select id=dp20amtpass class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Випадковий пароль" + '</option>' + keeppass + '<option value=2>' + "Виберіть новий пароль" + '</option></select>')
                x += '<div id=dp20amtpassdiv style=display:none>';
                x += addHtmlFormFloating("Новий пароль*", '<input id=dp20amtpolicypass type=password class="form-control" maxlength=32 placeholder="New password*" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy() autocomplete=off />');
                x += addHtmlFormFloating("Новий пароль*", '<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("Режим CCM", '<select id=dp20amtccmmode class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Не змінювати, зберегти CCM, якщо встановлено" + '</option><option value=1>' + "Деактивувати CCM, якщо встановлено" + '</option><option value=2>' + "Активувати CCM, якщо ACM зазнало невдачі" + '</option></select>'); }
                x += '<div id=dp20amtbadpassdiv style=display:none>';
                x += addHtmlFormFloating("Невідомий пароль", '<select id=dp20amtbadpass class="form-select"><option value=0>' + "Нічого не робити" + '</option><option value=1>' + "Якщо в CCM, повторно активувати Intel&reg; AMT" + '</option></select>');
                x += '</div>';
                if ((features & 0x400) == 0) { x += addHtmlFormFloating("Налаштування CIRA", '<select id=dp20amtcira class="form-select" title="' + "Клієнт Ініціював Віддалений Доступ" + '"><option value=0>' + "Нічого не робити" + '</option><option value=1>' + "Не підключатися до сервера" + '</option><option value=2>' + "Підключитися до сервера" + '</option></select>'); }
                x += '<span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 символів, 1 велика літера, 1 маленька літера, 1 цифра, 1 спеціальний знак." + '</span>';
                if ((currentMesh.mtype == 2) && (ptype == 2)) { x += '<span style="font-size:10px"> ' + "Ця політика не впливатиме на пристрої з Intel&reg; AMT у режимі 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">' + "Якщо вибрано цю політику, Intel&reg; AMT не керується цим сервером. Intel AMT усе ще можна використовувати, активувавши та налаштувавши його вручну." + '</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">' + "Якщо вибрано цю політику, будь-який Intel&reg; AMT у режимі клієнта (CCM) буде вимкнено. Інші пристрої із CIRA буде очищено, і ними можна буде керувати вручну." + '</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">' + "Це рекомендована політика. Активація та керування Intel&reg; AMT відбувається повністю автоматично, і сервер намагатиметься максимально ефективно використовувати можливості апаратного управління." + '</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("Ви впевнені, що бажаєте видалити групу {0}? Видалення групи пристроїв також видалить всю інформацію про пристрої в цій групі.", EscapeHtml(currentMesh.name)) + '<br /><br />';
            x += '<label class=form-check-label><input id=p20check type=checkbox class="form-check-input m-2" onchange=p20validateDeleteMeshDialog() />' + "Підтвердити" + '</label>';
            setModalContent('xxAddAgent', "Видалити Групу", 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 = "Немає пристроїв ретрансляції.";
                setModalContent('xxAddAgent', "Редагувати Групу Пристроїв", "Немає пристроїв ретрансляції.");
                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("Пристрій Ретрансляції", '<select id=d2devrelay class="form-select">' + relayDevices2.join('') + '</select>');
                setModalContent('xxAddAgent', "Редагувати Групу Пристроїв", 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="' + "Ім'я" + '" onchange=p20editmeshValidate() onkeyup=p20editmeshValidate(event) />';
            x += '<label for=dp20meshname>' + "Ім'я" + '</label>';
            x += '</div>';

            x += '<div class="form-floating mb-2">';
            x += '<textarea id=dp20meshdesc class="form-control" maxlength=1024 placeholder="' + "Опис" + '"></textarea>';
            x += '<label for=dp20meshdesc>' + "Опис" + '</label>';
            x += '</div>';
            setModalContent('xxAddAgent', "Редагувати Групу Пристроїв", 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 = "Редагувати Згоду Користувача Групи Пристроїв"; }
            if (editType == 2) { consent = (currentUser.consent) ? currentUser.consent : 0; title = "Редагувати Згоду Користувача"; }
            if (editType == 3) { consent = (currentNode.consent) ? currentNode.consent : 0; title = "Редагувати Згоду Користувача Пристрою"; }
            if (editType == 4) { consent = (currentUserGroup.consent) ? currentUserGroup.consent : 0; title = "Редагувати Згоду Користувача Групи Користувачів"; }
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px"><b>' + "Стільниця" + '</b></div>';
            x += '<div><label for=d20flag1><input type=checkbox id=d20flag1 class="form-check-input me-2" ' + ((consent & 0x0001) ? 'checked' : '') + '>' + "Сповістити користувача" + '</label></div>';
            x += '<div><label for=d20flag2><input type=checkbox id=d20flag2 class="form-check-input me-2" ' + ((consent & 0x0008) ? 'checked' : '') + '>' + "Запитувати згоду в користувача" + '</label></div>';
            x += '<div><label for=d20flag7><input type=checkbox id=d20flag7 class="form-check-input me-2" ' + ((consent & 0x0040) ? 'checked' : '') + '>' + "Показати панель підключення" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Термінал" + '</b></div>';
            x += '<div><label for=d20flag3><input type=checkbox id=d20flag3 class="form-check-input me-2" ' + ((consent & 0x0002) ? 'checked' : '') + '>' + "Сповістити користувача" + '</label></div>';
            x += '<div><label for=d20flag4><input type=checkbox id=d20flag4 class="form-check-input me-2" ' + ((consent & 0x0010) ? 'checked' : '') + '>' + "Запитувати згоду в користувача" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Файли" + '</b></div>';
            x += '<div><label for=d20flag5><input type=checkbox id=d20flag5 class="form-check-input me-2" ' + ((consent & 0x0004) ? 'checked' : '') + '>' + "Сповістити користувача" + '</label></div>';
            x += '<div><label for=d20flag6><input type=checkbox id=d20flag6 class="form-check-input me-2" ' + ((consent & 0x0020) ? 'checked' : '') + '>' + "Запитувати згоду в користувача" + '</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' : '') + '>' + "Запис сеансу" + '</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) ? "Синхронізувати назву пристрою сервера з назвою порту" : "Синхронізувати назву пристрою на сервері з іменем хоста") + '</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' : '') + '>' + "Надавати перевагу значенню --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' : '') + '>' + "Дозволити перевизначення імені пристрою сервера до наступного підключення" + '</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' : '') + '>' + "Видалити пристрій після відключення" + '</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' : '') + '>' + "Автоматично видаляти неактивні пристрої" + '</label><br></div>';
            x += '<div style=margin-left:20px id=d20expireDeviceDev>' + "Інактивувати дні до видалення" + ' <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>' + "Типово неактивні пристрої видаляються через 1 день." + '</span>', serverinfo.autoremoveinactivedevices);
                } else {
                    x += format('<span style=font-size:x-small;margin-left:6px>' + "Типово неактивні пристрої видаляються через {0} дн." + '</span>', serverinfo.autoremoveinactivedevices);
                }
            }

            setModalContent('xxAddAgent', "Редагування можливостей групи пристроїв", 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 += "Дозволити користувачам керувати цією групою пристроїв та пристроями, що входять до неї."; } else { x += "Дозволити користувачам керувати цим пристроєм."; }
                    if (features & 0x00080000) { x += " Користувачам потрібно підключитися хоча б раз до цього серверу, щоб мати можливість бути доданими у групи пристроїв." }
                    x += '<br /><br />';
                }
                x += '<div style=\'position:relative\'>';
                x += addHtmlFormFloating("Ідентифікатори користувачів", '<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("Група пристроїв", '<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("Група користувачів", '<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("Група користувачів", '<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("Група пристроїв", '<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("Група пристроїв", '<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("Пристрій", '<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("Групові дозволи для {0}.", uname) + '<br /><br />';
                } else {
                    x += format("Дозволи групи для користувача {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>' + "Повноправний Адміністратор" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editmesh>' + "Редагувати Групу Пристроїв" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20manageusers>' + "Керувати користувачами групи пристроїв" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20managecomputers>' + "Керувати комп'ютерами групи пристроїв" + '</label><br>';
            }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotecontrol>' + "Дистанційне керування та ретрансляція" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remoteview style=margin-left:12px>' + "Лише перегляд екрану" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotelimitedinput style=margin-left:12px>' + "Лише базові клавіші" + '</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>' + "Можливість ділитись з гостями" + '</label><br>'; }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nodesktop style=margin-left:12px>' + "Відключити доступ до стільниці" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noterminal style=margin-left:12px>' + "Відключити доступ до терміналу" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nofiles style=margin-left:12px>' + "Відключити доступ до файлів" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noamt style=margin-left:12px>' + "Відключити доступ до Intel&reg; AMT" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshagentconsole>' + "Консоль Mesh Agent" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshserverfiles>' + "Файли Сервера" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20wakedevices>' + "Пробудити Пристрої" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editnotes>' + "Редагувати Нотатки Пристрою" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20limitevents>' + "Тільки Показати Свої Дії" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20chatnotify>' + "Чат і Сповіщення" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20uninstall>' + "Видалити Агента / Видалити Пристрій" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20commands>' + "Віддалені Команди" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20resetoff>' + "Перезапустити / Вимкнути" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20details>' + "Деталі Пристрою" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20relay>' + "Використання ретрансляції" + '</label><br>';
            x += '</div>';

            if (userid == null) {
                xxdialogButtons = 3; xxdialogTag = null;
                setModalContent('xxAddAgent', "Додати Користувачів до Групи Пристроїв", 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) ? "Додайте Дозволи Групи Пристроїв" : "Редагувати Дозволи Групи Пристроїв", 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', "Додати Групу Користувачів", 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) ? "Додати Групу Пристроїв" : "Редагувати Групу Пристроїв", 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) ? "Додати Дозволи Пристрою" : "Редагувати Дозволи Пристрою", 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) ? "Додати Дозволи Пристрою" : "Редагувати Дозволи Пристрою", 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 ? "Редагувати Дозволи Пристрою Користувача" : "Додати Дозволи для Пристрою Користувача", 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 ? "Редагувати Дозволи Крупи Користувачів на Пристрої" : "Додати Дозволи на Пристрої Групи Користувачів", 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', "Редагувати Дозволи Групи Пристроїв", x);
                } else {
                    xxdialogButtons = 7, xxdialogTag = userid;
                    setModalContent('xxAddAgent', "Редагувати Дозволи Групи Пристроїв Користувача", 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("Повноправний Адміністратор (усі дозволи)"); else {
                    if ((meshrights & 1) != 0) r.push("Редагувати Групу Пристроїв");
                    if ((meshrights & 2) != 0) r.push("Керувати користувачами групи пристроїв");
                    if ((meshrights & 4) != 0) r.push("Керувати комп'ютерами групи пристроїв");
                    if ((meshrights & 8) != 0) r.push("Дистанційне керування");
                    if ((meshrights & 16) != 0) r.push("Консоль Агента");
                    if ((meshrights & 32) != 0) r.push("Файли Сервера");
                    if ((meshrights & 64) != 0) r.push("Пробудити Пристрої");
                    if ((meshrights & 128) != 0) r.push("Редагувати Примітки");
                    if (((meshrights & 8) != 0) && (meshrights & 256) != 0) r.push("Лише перегляд екрану");
                    if (((meshrights & 8) != 0) && (meshrights & 65536) != 0) r.push("Без стільниці");
                    if (((meshrights & 8) != 0) && (meshrights & 512) != 0) r.push("Без Терміналу");
                    if (((meshrights & 8) != 0) && (meshrights & 1024) != 0) r.push("Без файлів");
                    if (((meshrights & 8) != 0) && (meshrights & 2048) != 0) r.push("Відключити доступ до Intel&reg; AMT");
                    if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("Лише базові клавіші");
                    if ((meshrights & 8192) != 0) r.push("Лише події");
                    if ((meshrights & 16384) != 0) r.push("Чат і Сповіщення");
                    if ((meshrights & 32768) != 0) r.push("Видалити");
                    if ((meshrights & 131072) != 0) r.push("Команди");
                    if ((meshrights & 262144) != 0) r.push("Перезапустити/Вимкнути");
                    if ((meshrights & 524288) != 0) r.push("Поширити");
                    if ((meshrights & 1048576) != 0) r.push("Деталі");
                    if ((meshrights & 2097152) != 0) r.push("Ретрансляція");
                }
                if (r.length == 0) { r.push("Немає Дозволів"); }
                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("Ім'я користувача", EscapeHtml(decodeURIComponent(uname)));
                if (xuserid.split('/')[2] != uname) { x += addHtmlValue("Ідентифікатор користувача", EscapeHtml(xuserid.split('/')[2])); }

                x += addHtmlValue("Дозволи", r.join(", "));
                if (((userinfo._id) != xuserid) && (cmeshrights == 0xFFFFFFFF || (((cmeshrights & 2) != 0) && (meshrights != 0xFFFFFFFF)))) buttons += 4;
                setModalContent('xxAddAgent', "Користувач Групи Пристроїв", 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', "Видалити дозволи користувача", format("Підтвердити видалення прав користувача \"{0}\"?", EscapeHtml(decodeURIComponent(uname))));
            }
            if (userid.startsWith('ugrp/')) {
                setModalContent('xxAddAgent', "Видалити дозволи групи користувачів", format("Підтвердити видалення прав для групи користувачів \"{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 = "Коли ввімкнено, будь-хто може використовувати коди виклику, щоб приєднати пристрій до цієї групи пристроїв за допомогою такого публічного посилання:" + '<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() />' + "Увімкнути коди запрошення" + '</label></div>';
                x += addHtmlFormFloating("Коди запрошень", '<input id=agentInviteCode class="form-control" onkeyup=p20editmeshInviteCodeValidate() placeholder="code1, code2, code3" />');
                x += addHtmlFormFloating("Агент", '<select id=agentType class="form-select" onchange=p20editmeshInviteCodeValidate()><option value=0>' + "Усі доступні агенти" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "MeshCentral Асистент" + '</option></select>');
                x += '<div id=d2agentInstallTypeDiv>';
                x += addHtmlFormFloating("Тип Інсталяції", '<select id=agentInviteType class="form-select"><option value=0>' + "Фоновий та Інтерактивний" + '</option><option value=2>' + "Лише фоновий" + '</option><option value=1>' + "Лише інтерактивний" + '</option></select>');
                x += '</div>';
                setModalContent('xxAddAgent', "Коди запрошень", 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 = "Коди запрошень можуть використовуватися будь-ким, щоб приєднати пристрої до цієї групи пристроїв за допомогою наступного публічного лінка:" + '<br /><br />';
                x += '<div style=width:100%;text-align:center><a rel="noreferrer noopener" target=_blank href="' + url + '">' + url + '</a></div><br />';
                x += addHtmlValue("Коди запрошень", currentMesh.invite.codes.join(', '));
                x += addHtmlValue("Тип Інсталяції", ["Фоновий та Інтерактивний", "Лише інтерактивний", "Лише фоновий"][currentMesh.invite.flags & 3]);
                setModalContent('xxAddAgent', "Коди запрошень", 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">' + "Вебсповіщення" + '</div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Підключення пристрою" + '</label></div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Відключення пристрою" + '</label></div>';
            if (currentMesh.mtype < 3) {
                x += '<div class="form-check"><label><input id=p20notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Події Intel&reg; AMT для стільниці та послідовного порту" + '</label></div>';
            }
            if (emailNotify) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Сповіщення е-поштою" + '</div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Підключення пристрою" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Відключення пристрою" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceHelp class="form-check-input me-2" type=checkbox />' + "Запит допомоги" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Сповіщення месенджера" + '</div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceConnect class="form-check-input me-2" type=checkbox />' + "Підключення пристрою" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Відключення пристрою" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceHelp class="form-check-input me-2" type=checkbox />' + "Запит допомоги" + '</label></div>';
            }

            setModalContent('xxAddAgent', "Налаштування Сповіщень", 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: ["Не Підключено", "Агент", "Intel&reg; AMT", "Агент + Intel&reg; AMT"] },
                options: { responsive: true, plugins: { legend: { display: false } }, animation: { animateScale: true, animateRotate: true }, layout: { padding: { left: 10, right: 10, top: 10, bottom: 10 } } }
            });
            window.meshSecurityChart = new Chart(document.getElementById('meshSecurityChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#20F', '#40D', '#60B', '#809', '#A07', '#C05'] }], labels: ["OK", "Антивірус непрацюючий", "Немає автоматичного оновлення", "Фаєрвол не активний", "Декілька Проблем"] },
                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>' + "Немає" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span tabindex=0 title="' + "Клікніть тут, щоб змінити назву групи пристроїв" + '" 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>' + "Стани Живлення" + '</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>' + "Тип Агента" + '</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([["Не Підключено", "Агент", "Intel&reg; AMT", "Агент + 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>' + "З'єднання" + '</th></tr>';
                for (var i = 0; i < 4; i++) { if (xconnectivityStates[i][1] > 0) { x += '<tr style=' + (((++count % 2) == 0) ? 'background-color:#DDD' : '') + '><td style=text-align:right;width:60px>' + xconnectivityStates[i][1] + '<td> ' + xconnectivityStates[i][0] + '</tr>'; } }
                x += '</tbody></table>';
            }

            if (showSecurity) {
                count = 0;
                var xsecurityStates = [];
                for (var i = 0; i < 4; i++) {
                    xsecurityStates.push([[
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:ok")\' width=10 height=10 /> ' + "OK",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noav")\' width=10 height=10 /> ' + "Антивірус непрацюючий",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noupdate")\' width=10 height=10 /> ' + "Немає автоматичного оновлення",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:nofirewall")\' width=10 height=10 /> ' + "Фаєрвол не активний",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:any")\' width=10 height=10 /> ' + "Декілька Проблем"
                    ][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>' + "Безпека" + '</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>' + "У цій групі пристроїв немає пристроїв." + '</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)">' + "Root" + '</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="' + "Показати публічне посилання" + '" onclick=\'return p5showPublicLink("' + (publicPath + '/' + encodeURIComponentEx(f.nx)) + '?download=1' + loginkey + '")\'></i>';
                        publiclink += '<i class="fa-solid fa-download fa-fw" title="' + "Копіювати посилання до буфера обміну" + '" 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 "Ліміт сховища переповнено";
            if (bytes < 2048) return format("Залишилося {0} байт", bytes);
            if (bytes < 2097152) return format("Залишилося {0} кілобайт", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("Залишилося {0} мегабайт", Math.round(bytes / 1024 / 1024));
            return format("Залишилося {0} гігабайт", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize2(bytes) {
            if (bytes <= 0) return "Немає";
            if (bytes < 2048) return format("{0} Б", bytes);
            if (bytes < 2097152) return format("{0} кБ", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("{0} Мб", Math.round(bytes / 1024 / 1024));
            return format("{0} ГБ", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize3(bytes) {
            if (bytes <= 0) return "Немає";
            if (bytes < 2048) return format("{0} Б", bytes);
            if (bytes < 2097152) return format("{0} кБ", round(bytes / 1024, 1));
            if (bytes < 2147483648) return format("{0} Мб", round(bytes / 1024 / 1024, 1));
            return format("{0} ГБ", 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}кБ у {1} файлах. {2}кБ максимум", tf, f.c, (Math.floor(f.maxbytes / 1024 / 1024))); } else { title = format("{0}кБ у 1 файлу. {1}кБ максимум", 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', "Публічне Посилання", '<input type=text style=width:350px value="' + u + '" readonly /> <i class="fa-regular fa-copy" title="' + "Копіювати посилання до буфера обміну" + '" 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 ? "Скинути вибір" : "Обрати Все");
            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 байт"; return format("{0} байт", 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', "Нова Тека", '<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>' + "Рекурсивне видалення" + '</label><br>' : '<input type=checkbox id=p5recdeleteinput class="form-check-input me-2" style=\'display:none\'>';
            setModalContent('xxAddAgent', "Видалити", (cc > 1) ? (format("Видалити {0} відібраний(их) елемент(ів)?", cc) + rec) : ("Видалити відібраний елемент?" + 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', "Перейменувати", '<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', "Відвантажити файл", '<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\')" />' + "Підтвердити перезапис?" + '</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("Редактор файлів", "Можна редагувати лише файли розміром менше 200 кБ."); }
                    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("Підтвердити {0} із {1} записів{2} для цієї локації?", (p5clipboardCut == 0 ? 'copy' : 'move'), p5clipboard.length, ((p5clipboard.length > 1) ? 's' : '')) }
            setModalContent('xxAddAgent', "Вставити", 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("Утримано {0} запис{1} для {2}", p5clipboard.length, ((p5clipboard.length > 1) ? 's' : ''), (p5clipboardCut == 0 ? "Копіювати" : "Move")) + ', <a href=# onclick="return p5clearClip()" style=cursor:pointer>' + "Очистити" + '</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', "Відвантажити файл", format((overWriteCount == 1) ? "Передавання перезапише 1 файл. Продовжити?" : "Передавання перезапише {0} файлів. Продовжити?", 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: "Вхід до акаунту",
            2: "Вихід з акаунту",
            3: "Мову змінено з {1} на {2}",
            4: "Приєднався до мультиплексного сеансу стільниці", // No longer in use, replaced with 143
            5: "Припинити мультиплексний сеанс стільниці", // No longer in use, replaced with 144
            6: "Розпочато мультиплексний сеанс стільниці", // No longer in use, replaced with 145
            7: "Сеанс запису завершено, {0} секунд(и)", // No longer in use, replaced with 146
            8: "Закрито сесію мультиплексної стільниці, {0} секунд(и)", // No longer in use, replaced with 147
            9: "Завершено сесію ретрансляції \"{0}\" від {1} до {2}, {3} секунд(и)",
            10: "Завершено термінальну сесію \"{0}\" з {1} до {2}, {3} секунд(и)",
            11: "Завершено сесію стільниці \"{0}\" з {1} до {2}, {3} секунд(и)",
            12: "Завершено сесію керування файлами \"{0}\" з {1} до {2}, {3} секунд(и)",
            13: "Розпочато сеанс ретрансляції \"{0}\" від {1} до {2}",
            14: "Розпочато термінальний сеанс \"{0}\" від {1} до {2}",
            15: "Розпочато робочий сеанс \"{0}\" від {1} до {2}",
            16: "Розпочато сеанс керування файлами \"{0}\" від {1} до {2}",
            17: "Обробка консольної команди: \"{0}\"",
            18: "Показ вікна повідомлення, title=\"{0}\", message=\"{1}\"",
            19: "Процес {0} ({1}) Завершено",
            20: "Відкриття: {0}",
            21: "Отримання вмісту буфера, {0} байт(ів)",
            22: "Налаштування вмісту буфера, {0} байт(ів)",
            23: "Спроба активувати режим Intel(R) AMT ACM",
            24: "Запущені команди",
            25: "Виконання керування живленням={0}, примусове={1}",
            26: "Показувати висувне повідомлення, title=\"{0}\", message=\"{1}\"",
            27: "Локальний користувач прийняв запит віддаленого терміналу",
            28: "Локальний користувач відхилив запит віддаленого терміналу",
            29: "Локальний користувач примусово закрив підключення до віддаленої стільниці",
            30: "Запуск віддаленої стільниці після згоди локального користувача",
            31: "Панель підключення до віддаленої стільниці активовано/оновлено",
            32: "Панель підключення до віддаленої стільниці не працює або не підтримується",
            33: "Локальний користувач примусово закрив підключення до віддаленої стільниці", // No longer used, 29 must be used instead.
            34: "Не вдалося запустити віддалену стільницю через відмову локального користувача",
            35: "Віддалену стільницю запущено із висувним сповіщенням",
            36: "Віддалену стільницю запущено без сповіщення",
            37: "Панель підключення до віддаленої стільниці активовано/оновлено", // No longer used, 31 must be used instead.
            38: "Панель підключення до віддаленої стільниці не працює або не підтримується", // No longer used, 33 must be used instead.
            39: "Локальний користувач примусово закрив підключення до віддаленої стільниці", // No longer used, 29 must be used instead.
            40: "Запуск віддалених файлів після згоди локального користувача",
            41: "Не вдалося запустити віддалені файли після відмови локальним користувачем",
            42: "Запускати віддалені файли із висувним сповіщенням",
            43: "Запускати віддалені файли без сповіщення",
            44: "Створити теку: \"{0}\"",
            45: "Видалити: \"{0}\"",
            46: "Видалити рекурсивно: \"{0}\", {1} елемент(ів) видалено",
            47: "Видалити: \"{0}\", {1} елемент(ів) видалено",
            48: "Перейменувати: \"{0}\" на \"{1}\"",
            49: "Завантажити: \"{0}\"",
            50: "Передати: \"{0}\"",
            51: "Копіювати: \"{0}\" до \"{1}\"",
            52: "Перемістити: \"{0}\" до \"{1}\"",
            53: "Блокування віддаленого користувача на його стільниці",
            54: "Агент закрив сесію зі стисненням {0}% між агентом і сервером. Надіслано: {1}, стиснуто: {2}",
            55: "Створена група пристроїв: {0}",
            56: "Відновлено групу пристроїв: {0}",
            57: "Пристрій {0} додано до групи пристроїв {1}",
            58: "Пристрій запитав активацію Intel(R) AMT ACM, FQDN: {0}",
            59: "Пристрій {0} змінено з групи {1}: {2}",
            60: "Права користувача пристрою видалено для {0}",
            61: "Права користувача пристрою змінено для {0}",
            62: "Користувача {0} видалено з групи користувачів {1}",
            63: "Акаунт видалено",
            64: "Акаунт створено із іменем користувача {0}",
            65: "Акаунт створено з е-поштою {0}",
            66: "Акаунт змінено: {0}",
            67: "Змінено членство в групі користувачів: {0}",
            68: "Групу користувачів {0} додано до групи пристроїв {1}",
            69: "Створена група користувачів: {0}",
            70: "Групу користувачів {0} видалено з групи пристроїв {1}",
            71: "Додано користувача {0} до групи користувачів {1}",
            72: "Користувача {0} видалено з групи користувачів {1}",
            73: "Сповіщення групи пристроїв змінено",
            74: "Пароль до акаунту змінено: {0}",
            75: "Змінено облікові дані акаунту",
            76: "Створено групу пристроїв: {0}",
            77: "Групу пристроїв видалено: {0}",
            78: "Змінено належність у групі пристроїв: {0}",
            79: "Змінено групу користувачів: {0}",
            80: "Додано користувача {0} до групи користувачів {1}",
            81: "Права користувача пристрою видалено для {0}",
            82: "Права користувача пристрою змінено для {0}",
            83: "Користувача {0} видалено з групи пристроїв {1}",
            84: "Пристрій {0} додано до групи пристроїв {1}",
            85: "Пристрій {0} переміщено до групи {1}",
            86: "Права користувача пристрою видалено для {0}",
            87: "Пристрій {0} видалено з групи пристроїв {1}",
            88: "Увімкнено двофакторну автентифікацію е-поштою",
            89: "Двофакторну автентифікацію е-поштою вимкнено",
            90: "Додано застосунок автентифікації",
            91: "Програму автентифікації видалено",
            92: "Нові резервні коди 2FA згенеровані",
            93: "ДФА резервні коди очищено",
            94: "Ключ безпеки видалено",
            95: "Додано Ключ Безпеки",
            96: "Підтверджений номер телефону користувача {0}",
            97: "Видалено номер телефону користувача {0}",
            98: "Запит допомоги, користувач: {0}, деталі: {1}",
            99: "Виконання команд від імені користувача",
            100: "Виконання команд від імені користувача, якщо можливо",
            101: "Додано поширення пристрою {0} від {1} до {2}",
            102: "Спільний доступ до пристрою видалено {0}",
            103: "Пакетне завантаження файлів ({0}) до теки {1}",
            104: "Автоматичне завантаження файлу дампа ядра агента: \"{0}\"",
            105: "Передати: \"{0}\", Розмір: {1}",
            106: "Завантажити: \"{0}\", Розмір: {1}",
            107: "Вхід до акаунту з {0}, {1}, {2}",
            108: "Спроба входу користувача з некоректною двофакторною автентифікацією {0}, {1}, {2}",
            109: "Спроба входу користувача до заблокованого акаунту {0}, {1}, {2}",
            110: "Хибна спроба лоґіну користувача з {0}, {1}, {2}",
            111: "Пристрій запитав активацію Intel(R) AMT ACM TLS, FQDN: {0}",
            112: "Завершено сесію месенджера \"{0}\" з {1} до {2}, {3} секунд(и)",
            113: "На пристрій додано автентифікацію через push-сповіщення",
            114: "Видалено пристрій автентифікації push-повідомлень",
            115: "Додано токен лоґіну",
            116: "Токен входу видалено",
            117: "Це стара версія агента, розгляньте можливість оновлення.",
            118: "Цей агент має застарілий механізм перевірки сертифіката, розгляньте можливість оновлення.",
            119: "Цей агент використовує незахищені тунелі, розгляньте можливість оновлення.",
            120: "Розпочато локальний сеанс ретрансляції \"{0}\", протокол {1} ​​до {2}",
            121: "Завершено сесію локальної ретрансляції \"{0}\", протокол {1} ​​до {2}, {3} секунд(и)",
            122: "Припинення мультиплексного сеансу стільниці через {0} секунд(и).", // No longer in use, replaced with 144
            123: "Залишити сеанс Web-SSH \"{1}\" через {0} секунд(и).",
            124: "Залишити сеанс Web-SFTP \"{1}\" через {0} секунд(и).",
            125: "Залишити сеанс Web-RDP \"{1}\" через {0} секунд(и).",
            126: "Залишити сеанс Web-VNC через {0} секунд(и).",
            127: "Відображене ім'я акаунту змінено на {0}.",
            128: "Акаунт створено із іменем {0}.",
            129: "Відображене ім'я акаунту видалено.",
            130: "Сповіщення користувачів змінено",
            131: "Додано поширення пристрою {0} без обмеження часу.",
            132: "Увімкнути.",
            133: "Вимкнути.",
            134: "Примусово перервано робочий сеанс користувача {0}",
            135: "Сеанс терміналу користувача {0} примусово припинено",
            136: "Примусово перервано файловий сеанс користувача {0}",
            137: "Примусове відключення сеансу маршрутизації користувача {0}",
            138: "Додано поширення пристрою {0}, із щоденною переактивацією.",
            139: "Додано поширення пристрою {0}, із щотижневою переактивацією.",
            140: "Пристрій {0} змінено з групи {1}: {2}",
            141: "Зміна полісі Intel(r) AMT",
            142: "Групу пристроїв {0} змінено: {1}",
            143: "Приєднався до мультиплексного сеансу стільниці \"{0}\"",
            144: "Припинення мультиплексного сеансу \"{0}\" через {1} секунд(и).",
            145: "Розпочато мультиплексний сеанс стільниці \"{0}\"",
            146: "Сеанс запису завершено \"{0}\", {1} секунд(и)",
            147: "Закрито сесію мультиплексної стільниці \"{0}\", {1} секунд(и)",
            148: "Розпочато сеанс Web-SSH \"{0}\".",
            149: "Розпочато сеанс Web-SFTP \"{0}\".",
            150: "Розпочато сеанс Web-RDP \"{0}\".",
            151: "Розпочато сеанс Web-VNC \"{0}\".", // Not in use yet
            152: "Більше не ретранслятор для \"{0}\".",
            153: "Є ретранслятором для \"{0}\".",
            154: "Акаунт змінено на синхронізацію з даними LDAP.",
            155: "Відмовлено у вході користувачу з {0}, {1}, {2}",
            156: "Обліковий запис месенджера {0} верифіковано",
            157: "токен входу видалено",
            158: "Показ вікна попередження, title=\"{0}\", message=\"{1}\"",
            159: "Пристрій Увімкнено",
            160: "Enabled Duo two-factor authentication",
            161: "Disabled Duo two-factor authentication",
            162: "Розпочато сеанс месенджера \"{0}\" від {1} до {2}",
            163: "Пристрій {0} видалено з групи користувачів {1}",
            164: "Створити файл: \"{0}\""
        };

        var eventsShortMessageId = {
            1: "Ім'я групи",
            2: "Опис",
            3: "Прапорці",
            4: "Згода",
            5: "Авто-Видалення",
            6: "Код запрошення",
            7: "Пристрій Рестранслятор"
        }

        // 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="' + "Це гостьовий сеанс спільного доступу" + '">' + 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>' + "Подій не знайдено" + '</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("Тип звіту", '<select id=d2reportType class="form-select" onchange=p3showReportValidation()><option value=1>' + "Усі Події" + '</option>');
            x += addHtmlFormFloating("Проміжок часу", '<select id=d2reportSpan class="form-select" onchange=p3showReportValidation()><option value=1>' + "Усі Наявні" + '</option><option value=2>' + "Один День" + '</option></select>');
            x += '<div id=d2daySelector>' + addHtmlFormFloating("Звітний день", '<input type=date id=d2reportDay class="form-control" onchange=p3showReportValidation()></input>') + '</div>';
            x += addHtmlFormFloating("Групувати як", '<select id=d2reportGroupBy class="form-select" onchange=p3showReportValidation()><option value=1>' + "Немає" + '</option><option value=2>' + "Група пристроїв" + '</option><option value=3>' + "Користувач" + '</option>');
            x += addHtmlFormFloating("Формат", '<select id=d2reportFormat class="form-select" onchange=p3showReportValidation()><option value=1>' + "CSV" + '</option><option value=2>' + "JSON" + '</option>');
            x += '</div>';
            setModalContent('xxAddAgent', "Завантажити Звіт", 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 = "Завантажте файл переліку подій в одному з наведених нижче форматів." + '<br /><br />';
            x += addHtmlValue("Формат CSV", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogCSV(' + mode + ')">' + 'eventslist.csv' + '</a>');
            x += addHtmlValue("Формат JSON", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogJSON(' + mode + ')">' + 'eventslist.json' + '</a>');
            setModalContent('xxAddAgent', "Експортувати Перелік Подій", 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, час, тип, дія, користувач, пристрій, повідомлення" + '\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>' + "Ім'я" + '<th style=width:80px>' + "Групи Пристроїв" + '<th style=width:120px>' + nobreak("Останній Доступ") + '<th style=width:120px>' + "Дозволи";

            // 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>' + "Користувачі Онлайн"; 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>' + "Офлайн Користувачі"; addHeader = false; }
                        x += addUserHtml(user, sessions);
                        maxUsers--;
                    } else {
                        hiddenUsers++;
                    }
                }
            }
            x += '</table>';
            if (hiddenUsers == 1) { x += '<br />' + "ще 1 користувача не показано, використовуйте поле пошуку для перегляду користувачів..." + '<br />'; }
            else if (hiddenUsers > 1) { x += '<br />' + format("{0} користувачів не показано, використовуйте вікно пошуку для пошуку користувачів...", hiddenUsers) + '<br />'; }
            if (maxUsers == displayUsersLimit) { x += '<br />' + "Користувачів не знайдено." + '<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=' + "Чат" + '><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 сесія"); } else { lastAccess += nobreak(format("{0} сеанс", sessions)); }
            } else {
                if (user.access) { lastAccess += '<span title="' + format("Останній доступ: {0}", printDateTime(new Date(user.access * 1000))) + '">' + printDate(new Date(user.access * 1000)) + '</span>'; }
                else if (user.login) { lastAccess += '<span title="' + format("Минулий вхід: {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 += "Заблоковано" + ',&nbsp;'; }
            permissions += '<span title=\'' + "Дозволи серверу" + '\'>';

            var urights = user.siteadmin & (0xFFFFFFFF - 1248);
            if ((user.siteadmin == null) || (urights == 0)) {
                permissions += "Користувач";
            } else if (urights == 8) {
                permissions += "Користувач + Файли";
            } else if (user.siteadmin == 0xFFFFFFFF) {
                permissions += "Адміністратор";
            } else if ((urights & 2) != 0) {
                permissions += "Менеджер";
            } else {
                permissions += "Частково";
            }
            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="' + "Е-пошта не верифікована" + '">&#x2717;</b>' : ' <b style=color:green title="' + "Е-пошту верифіковано" + '">&#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="' + "Надіслати електронний лист користувачеві" + '" 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="' + "Двофакторна автентифікація увімкнена" + '" style="margin-top:2px"></i>'; }
            if (user.phone != null) { username += ' <i class="fa-solid fa-mobile-screen" title="' + "Верифіковано номер телефону" + '" 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="' + "Акаунт заблоковано" + '" style="margin-top:2px" />'; }
            if ((user.msghandle != null) && (features2 & 0x02000000)) { username += ' <i class="fa-solid fa-comment fa-xs" title="' + "Обліковий запис месенджера верифіковано" + '" 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) ? "Скинути вибір" : "Обрати Все";
        }

        // 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>' + "Перевірка електронної пошти" + '</option><option value=5>' + "Хибна е-пошта" + '</option>'; }

            var x = "Виберіть операцію для виконання з усіма вибраними користувачами." + '<br /><br />';
            x += addHtmlFormFloating("Операція", '<select id=d3groupop class=form-select><option value=1>' + "Заблокувати акаунт" + '</option><option value=2>' + "Розблокувати обліковий запис" + '</option>' + optionalActions + '<option value=3>' + "Видалити акаунт" + '</option></select>');
            setModalContent('xxAddAgent', "Групова дія", 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 = "Підтвердити видалення відібраних акаунтів?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p3usersGroupActionFunctionDelCheck() />' + "Підтвердити" + '</label>';
                setModalContent('xxAddAgent', "Видалити Акаунти", 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', "Надіслати 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', "Надіслати повідомлення", '<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="' + "Тема" + '"></input>';
            x += '<textarea id=d2emailText maxlength=10000 style="width:100%;height:200px;resize:none;overflow-y:auto" onKeyUp=showSendEmailValidate()></textarea>';

            setModalContent('xxAddAgent', "Надіслати е-пошту", 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>' + "Надіслати текстове сповіщення цьому користувачеві." + '</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>' + "Показувати повідомлення, доки користувач не закриє його" + '</option>';
            x += '<option value=10>' + "Показувати 10 секунд" + '</option>';
            x += '<option value=60>' + "Показувати 1 хвилину" + '</option>';
            x += '<option value=300>' + "Показувати 5 хвилин" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', format("Сповіщення {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') + '>' + "Показувати лише перші 100 користувачів" + '</label><br />';
            setModalContent('xxAddAgent', "Перегляд користувачів", 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 = "Створіть кілька акаунтів одночасно, імпортувавши файл JSON або CSV" + '<br /><br />';
            x += "Формат файлу JSON наведено далі:" + '<br />';
            x += '<pre>[\r\n {"user":"x1","pass":"x","email":"x1@x"},\r\n {"user":"x2","pass":"x","resetNextLogin":true}\r\n]</pre><br />';
            x += "Формат файлу CSV нижче:" + '<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', "Імпортувати Акаунт Користувача", 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', "Імпортувати Акаунт Користувача", "Хибний формат файлу CSV.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Імпортувати Акаунт Користувача", "Хибний формат файлу CSV.");
                    }
                } else {
                    try {
                        j = JSON.parse(r.target.result);
                    } catch (ex) {
                        setModalContent('xxAddAgent', "Імпортувати Акаунт Користувача", format("Хибний файл JSON: {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', "Імпортувати Акаунт Користувача", "Хибний формат файлу JSON.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Імпортувати Акаунт Користувача", "Хибний формат файлу JSON.");
                    }
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            };
            fr.readAsText(Q('d4importFile').files[0]);
        }

        function p4downloadUserInfo() {
            if (xxdialogMode) return;
            var x = "Завантажити файл із переліком користувачів в одному з наведених нижче форматів." + '<br /><br />';
            x += addHtmlValue("Формат CSV", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoCSV()\'>' + 'userlist.csv' + '</a>');
            x += addHtmlValue("Формат JSON", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoJSON()\'>' + 'userlist.json' + '</a>');
            setModalContent('xxAddAgent', "Експортувати список користувачів", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

        function p4downloadUserInfoCSV() {
            var csv = "id,ім'я,е-пошта,створення,останній вхід,групи,параметри автентифікації,siteadmin,userradmin,заблоковано" + '\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>' + "Розсилка повідомлення всім підключеним користувачам." + '</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>' + "Показувати повідомлення, доки користувач не закриє його" + '</option>';
            x += '<option value=10>' + "Показувати 10 секунд" + '</option>';
            x += '<option value=60>' + "Показувати 1 хвилину" + '</option>';
            x += '<option value=300>' + "Показувати 5 хвилин" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', "Розсилка повідомлень", 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] == '') ? "Типовий" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Домен", y);
            }

            if ((features & 0x200000) == 0) { x += addHtmlFormFloating('<span id=p4hname>' + "Ім'я користувача" + '</span>', '<input id=p4name class="form-control" maxlength=64 autocomplete=username onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />'); }
            x += addHtmlFormFloating('<span id=p4hemail>' + "е-пошта", '<input id=p4email type=email class="form-control" maxlength=256 autocomplete="email" inputmode="email" onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />');
            x += '';
            x += addHtmlFormFloating('<span id=p4hp1>' + "Пароль" + '</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" />' + "Випадковий пароль." + '</label></div>';
            x += '<div><label><input id=p4removeEvents onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Видалити всі попередні події для цього ідентифікатора користувача." + '</label></div>';
            x += '<div><label><input id=p4resetNextLogin onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Примусово скинути пароль під час наступного входу." + '</label></div>';
            if (serverinfo.emailcheck) {
                x += '<div><label><input id=p4verifiedEmail onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Е-пошту верифіковано." + '</label></div>';
                x += '<div><label><input id=p4invitationEmail type=checkbox class="form-check-input me-2" title=' + "Потрібно верифікувати е-пошту та примусово скинути пароль." + ' />' + "Надіслати запрошення е-поштою." + '</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("Вимоги: {0}.", r.join(', ')) + '</div>'; }
            }

            setModalContent('xxAddAgent', "Створити Акаунт", 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 = "Введіть розділений комами список імен адміністративних областей." + '<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', "Адміністративні Області", 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>' + "Файли Сервера" + '</label>, <input type=number class="me-1 form-control" onchange=showUserAdminDialogValidate() maxlength=10 id=ua_fileaccessquota>' + "k max, порожнє за замовчуванням" + '</div><br><hr/>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_fulladmin>' + "Повноправний Адміністратор" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverbackup>' + "Резервне копіювання сервера" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverrestore>' + "Відновлення сервера" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverupdate>' + "Оновлення сервера" + '</label><br></div>';
            x += '<div id=d2MngUsr><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusers>' + "Керувати користувачами" + '</label><br></div>';
            x += '<div id=d2MngGrp><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusergroups>' + "Керувати групами користувачів" + '</label><br></div>';
            x += '<div id=d2MngRec><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_managerecordings>' + "Керувати записами" + '</label><br></div>';
            x += '<div id=d2AllEvnt><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_allevents>' + "Переглядати всі події" + '</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>' + "Заблокувати Акаунт" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewgroups>' + "Заборонити створення груп пристроїв" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewdevices>' + "Заборонити додавання нових пристроїв" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nomeshcmd>' + "Заборонити доступ до інструментів (MeshCmd/Router)" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_locksettings>' + "Закрити доступ до налаштувань акаунту" + '</label><br>';
            x += '</div>';

            setModalContent('xxAddAgent', "Дозволи серверу", 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 />' + "Групи не знайдено." + '<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>' + "Ім'я" + '<th style=width:80px>' + "Користувачі" + '<th style=width:80px>' + "Групи Пристроїв" + '<th style=width:80px>' + "Пристрої";
                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) ? "Скинути вибір" : "Обрати Все";
        }

        // 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 = "Виберіть операцію для виконання з усіма вибраними користувачами." + '<br /><br />';
            x += addHtmlFormFloating("Операція", '<select id=d50groupop class=form-select><option value=1>' + "Видалити групу" + '</option></select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Групова дія", 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 = "Підтвердити видалення відібраних груп користувачів?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p50usersGroupActionFunctionDelCheck() />' + "Підтвердити" + '</label>';
                setModalContent('xxAddAgent', "Видалити Групи Користувачів", 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("Група користувачів", '<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] == '') ? "Типовий" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Домен", y);
            }
            x += addHtmlFormFloating("Ім'я", '<input id=p4name class=form-control maxlength=64 onchange=showCreateUserGroupDialogValidate() onkeyup=showCreateUserGroupDialogValidate() />');
            x += addHtmlFormFloating("Опис", '<textarea id=p4desc class=form-control value="" maxlength=1024 /></textarea>');
            setModalContent('xxAddAgent', (mode == 1) ? "Створити Групу Користувачів" : "Дублювати Групу Користувачів", 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>' + "Немає" + '</i>'; }
            if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { gname = '<span role=button tabindex=0 title="' + "Клікніть тут, щоб змінити назву групи користувачів" + '" 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>' + "Немає" + '<i>'; } else { desc = EscapeHtml(desc); }

            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Ім'я", 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("Домен", (d != '') ? EscapeHtml(d) : ('<i>' + "Типовий" + '</i>'));
                x += addDeviceAttribute("Ідентифікатор групи", EscapeHtml(group._id));
            }
            if (currentUserGroup.membershipType != null) {
                x += addDeviceAttribute("Тип Групи", EscapeHtml(currentUserGroup.membershipType));
            }
            if ((userinfo.siteadmin & 256) != 0) {
                x += addDeviceAttribute("Опис", '<span role=button onclick=p51editgroup(2,' + (currentUserGroup.membershipType != null) + ')>' + desc + ' <i class="fa-solid fa-pencil fa-xs"/></i></span>');
            } else {
                x += addDeviceAttribute("Опис", desc);
            }

            // Display features
            if (serverinfo.userGroupsSessionRecording == 1) {
                var userGroupFeatures = [];
                if ((group.flags) && (group.flags & 2)) { userGroupFeatures.push("Запис Сеансів"); }
                userGroupFeatures = userGroupFeatures.join(', ');
                if (userGroupFeatures == '') { userGroupFeatures = '<i>' + "Немає" + '</i>'; }
                x += addDeviceAttribute("Можливості", 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("Запит до Стільниці та Панелі Засобів"); } else if (consent & 0x0040) { consentOptionsStr.push("Стільниця: панель підключення"); } else if (consent & 0x0008) { consentOptionsStr.push("Стільниця: запит"); } else { if (consent & 0x0001) { consentOptionsStr.push("Стільниця: сповістити"); } }
                if (consent & 0x0010) { consentOptionsStr.push("Термінал: запит"); } else { if (consent & 0x0002) { consentOptionsStr.push("Термінал: сповістити"); } }
                if (consent & 0x0020) { consentOptionsStr.push("Файли: запит"); } else { if (consent & 0x0004) { consentOptionsStr.push("Файли: сповістити"); } }
                if (consent == 7) { consentOptionsStr = ["Завжди сповіщати"]; }
                if ((consent & 56) == 56) { consentOptionsStr = ["Завжди запитувати"]; }

                consentOptionsStr = consentOptionsStr.join(', ');
                if (consentOptionsStr == '') { consentOptionsStr = '<i>' + "Немає" + '</i>'; }
                x += addDeviceAttribute("Згода користувача", addLinkConditional(consentOptionsStr, 'p20editmeshconsent(4)', true));
            }

            x += addDeviceAttribute("Користувачі", usercount);
            x += addDeviceAttribute("Групи Пристроїв", meshcount);
            x += addDeviceAttribute("Пристрої", devicecount);

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

            if ((userinfo.siteadmin & 256) != 0) {
                x += '<input type=button class="btn btn-primary btn-sm" value="' + "Розсилка" + '" title="' + "Надіслати сповіщення всім користувачам цієї групи." + '" 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> ' + "Додати Користувачів" + '</button>';
            }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Учасники Групи" + '</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="' + "Видалити права користувача для цієї групи пристроїв" + '" 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="' + "Користувач" + '"></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>' + "Немає Учасників" + '</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> ' + "Додати Групу Пристроїв" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Пов'язані Групи Пристроїв" + '</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>' + "Невідома Група Пристроїв" + '</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="' + "Видалити права групи користувачів на цю групу пристроїв" + '" 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="' + "Група пристроїв" + '" 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>' + "Немає пов'язаних груп пристроїв" + '</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> ' + "Додати Пристрій" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Пов'язані Пристрої" + '</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>' + "Невідомий Пристрій" + '</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="' + "Видалити права групи користувачів на цьому пристрої" + '" 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="' + "Група пристроїв" + '" 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>' + "Немає пов'язаних пристроїв" + '</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>' + "Видалити Групу Користувачів" + '</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' : '') + '>' + "Запис сеансу" + '</label><br></div>';
            }
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Редагувати Ознаки Групи Користувачів", 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', "Видалити дозволи пристрою", format("Підтвердити видалення прав доступу для пристрою \"{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', "Видалити дозволи групи пристроїв", format("Підтвердити видалення прав доступу для групи пристроїв \"{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("Ім'я", '<input id=dp51name class="form-control" maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) ' + (nameReadOnly ? 'readonly disabled' : '') + '/>');
            x += addHtmlFormFloating("Опис", '<textarea id=dp51desc class="form-control" maxlength=1024 style=width:100%;resize:none></textarea>');

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Редагувати Групу Користувачів", 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("Видалити групу користувачів {0}?", EscapeHtml(currentUserGroup.name)) + '<br /><br />';
            x += '<label><input id=p51check type=checkbox class="form-check-input me-2" onchange=p51validateDeleteGroupDialog() />' + "Підтвердити" + '</label>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Видалити Групу Користувачів", 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', "Видалити приналежність користувача", format("Підтвердити видалення приналежності у користувача \"{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 = "Дозволити користувачам керувати цією групою пристроїв та пристроями, що входять до неї.";
            if (features & 0x00080000) { x += " Користувачам потрібно підключитися хоча б раз до цього серверу, щоб мати можливість бути доданими у групи пристроїв." }
            x += '<br /><br /><div style=\'position:relative\'>';
            x += addHtmlFormFloating("Ідентифікатори користувачів", '<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', "Додати Користувачів до Групи Користувачів", 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="' + "Акаунт заблоковано" + '" style="margin-top:2px" /> '; msg.push("Заблокований акаунт"); }
            if ((user.siteadmin == null) || ((user.siteadmin & (0xFFFFFFFF - 1248)) == 0)) { msg.push("Немає дозволів на сервер"); } else if (user.siteadmin == 8) { msg.push("Доступ до файлів серверу"); } else if (user.siteadmin == 0xFFFFFFFF) { msg.push("Повноправний Адміністратор"); } else { msg.push("Часткові дозволи"); }
            if ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & (64 + 128 + 1024)) != 0)) { msg.push("Обмеження"); }

            // Show user attributes
            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Ім'я", user.name); } // If title bar is hidden, display the user name here
            var email = user.email ? EscapeHtml(user.email) : '<i>' + "Не визначено" + '</i>', everify = '';
            var realname = user.realname ? EscapeHtml(user.realname) : '<i>' + "Не визначено" + '</i>';
            if (serverinfo.emailcheck) { everify = ((user.emailVerified == true) ? '<b style=color:green;cursor:pointer title="' + "Е-пошту верифіковано" + '">&#x2713</b> ' : '<b style=color:red;cursor:pointer title="' + "Е-пошта не верифікована" + '">&#x2717;</b> '); }

            if ((serverinfo.crossDomain) || (debugmode != 0)) {
                var d = user._id.split('/')[1];
                x += addDeviceAttribute("Домен", ((d != '') ? EscapeHtml(d) : ('<i>' + "Типовий" + '</i>')));
                x += addDeviceAttribute("Ідентифікатор користувача", EscapeHtml(user._id));
            } else {
                if (user.name.toLowerCase() != user._id.split('/')[2]) { x += addDeviceAttribute("Ідентифікатор користувача", 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("е-пошта", '<span role=button onclick=p30showUserEmailChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + everify + email + emailLink + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
                x += addDeviceAttribute("Справжнє Ім'я", '<span role=button onclick=p30showUserRealNameChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + realname + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            } else {
                x += addDeviceAttribute("е-пошта", everify + email + emailLink);
                x += addDeviceAttribute("Справжнє Ім'я", realname);
            }

            if ((features & 0x02000000) || (user.phone != null)) { // If SMS is enabled on the server or user has a phone number
                x += addDeviceAttribute("Номер Телефону", '<span role=button onclick=p30editPhone()>' + (user.phone ? user.phone : ('<i>' + "Немає" + '</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("Обмін повідомленнями", '<span role=button onclick=p30editMessaging()><i class="fa-solid fa-comment fa-xs" title="' + "Обмін повідомленнями ввімкнено" + '" style="margin-top:2px"></i> ' + (user.msghandle ? user.msghandle : ('<i>' + "Немає" + '</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("Запис Сеансів"); }
            if (user.removeRights) {
                if ((user.removeRights & 0x00000008) != 0) { userFeatures.push("Відключити віддалене керування"); } else {
                    if ((user.removeRights & 0x00010000) != 0) { userFeatures.push("Без стільниці"); }
                    else if ((user.removeRights & 0x00000100) != 0) { userFeatures.push("Лише Перегляд Стільниці"); }
                    if ((user.removeRights & 0x00000200) != 0) { userFeatures.push("Без Терміналу"); }
                    if ((user.removeRights & 0x00000400) != 0) { userFeatures.push("Без файлів"); }
                }
                if ((user.removeRights & 0x00000010) != 0) { userFeatures.push("Без консолі"); }
                if ((user.removeRights & 0x00008000) != 0) { userFeatures.push("Без доступу до деінсталяції агента"); }
                if ((user.removeRights & 0x00020000) != 0) { userFeatures.push("Без віддалених команд"); }
                if ((user.removeRights & 0x00000040) != 0) { userFeatures.push("Без доступу до пробудження"); }
                if ((user.removeRights & 0x00040000) != 0) { userFeatures.push("Без доступу до перезапуску/вимкнення"); }
            }
            userFeatures = userFeatures.join(', ');
            if (userFeatures == '') { userFeatures = '<i>' + "Немає" + '</i>'; }
            x += addDeviceAttribute("Можливості", addLink(userFeatures, 'p20edituserfeatures()'));

            x += addDeviceAttribute("Права", '<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("Квота серверу", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
            x += addDeviceAttribute("Створено", printDateTime(new Date(user.creation * 1000)));
            if (user.login) x += addDeviceAttribute("Останній вхід", printDateTime(new Date(user.login * 1000)));
            if (user.passchange == -1) { x += addDeviceAttribute("Пароль", "Буде змінено під час наступного входу."); }
            else if (user.passchange) { x += addDeviceAttribute("Пароль", format("Востаннє змінено: {0}", printDateTime(new Date(user.passchange * 1000)))); }

            // Device Groups
            var linkCount = 0, linkCountStr = '<i>' + "Немає" + '<i>';
            if (user.links) {
                for (var i in user.links) { if (i.startsWith('mesh/')) { linkCount++; } }
                if (linkCount == 1) { linkCountStr = "1 група"; } else if (linkCount > 1) { linkCountStr = format("{0} груп", linkCount); }
            }
            x += addDeviceAttribute("Групи Пристроїв", linkCountStr);

            // Administrative Realms
            if ((userinfo.siteadmin == 0xFFFFFFFF) || (userinfo.siteadmin & 2)) {
                var xuserGroups = '<i>' + "Немає" + '</i>';
                if (user.groups) { xuserGroups = ''; for (var i in user.groups) { xuserGroups += '<span class="tagSpan">' + EscapeHtml(user.groups[i]) + '</span>'; } }
                x += addDeviceAttribute("Області Адміністратора", 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("Запит до Стільниці та Панелі Засобів"); } else if (consent & 0x0040) { meshFeatures.push("Стільниця: панель підключення"); } else if (consent & 0x0008) { meshFeatures.push("Стільниця: запит"); } else { if (consent & 0x0001) { meshFeatures.push("Стільниця: сповістити"); } }
                if (consent & 0x0010) { meshFeatures.push("Термінал: запит"); } else { if (consent & 0x0002) { meshFeatures.push("Термінал: сповістити"); } }
                if (consent & 0x0020) { meshFeatures.push("Файли: запит"); } else { if (consent & 0x0004) { meshFeatures.push("Файли: сповістити"); } }
                if (consent == 7) { meshFeatures = ["Завжди сповіщати"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Завжди запитувати"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Немає" + '</i>'; }
                x += addDeviceAttribute("Згода користувача", 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("Застосунок Автентифікації"); }
                if (user.otphkeys > 0) { factors.push("Ключ безпеки"); }
                if (user.otpekey > 0) { factors.push("е-пошта"); }
                if (user.otpduo > 0) { factors.push("Duo"); }
                if (user.otpkeys > 0) { factors.push("Резервні Коди"); }
                if (user.otpdev > 0) { factors.push("Push Пристрій"); }
                if ((user.phone != null) && (features & 0x04000000)) { factors.push("SMS"); }
                if ((user.msghandle != null) && (features2 & 0x04000000)) { factors.push("Обмін повідомленнями"); }
                x += addDeviceAttribute("Безпека", '<i class="fa-solid fa-key" title="' + "Двофакторна автентифікація увімкнена" + '" 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="' + "Примітки" + '" title="' + "Переглянути нотатки про цього користувача" + '" 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="' + "Надіслати SMS цьому користувачеві" + '" onclick=showSendSMS("' + encodeURIComponentEx(user._id) + '") />'; }
            if (user.msghandle && (features2 & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Повідомлення" + '" title="' + "Надіслати повідомлення цьому користувачеві" + '" 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="' + "е-пошта" + '" title="' + "Надіслати електронного листа цьому користувачеві" + '" 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="' + "Сповістити" + '" title="' + "Надіслати сповіщення користувачеві" + '" onclick=showUserAlertDialog(event,"' + encodeURIComponentEx(user._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Чат" + '" title="' + "Чат" + '" 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="' + "Видалити цього користувача" + '">' + "Видалити Користувача" + '</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="' + "Змінити пароль для цього користувача" + '">' + "Змінити Пароль" + '</a>';
                x += ' <a href=# style=cursor:pointer onclick=\'return p30viewPreviousLogins()\' title="' + "Переглянути попередні входи цього користувача" + '">' + "Попередні спроби входу" + '</a>';
            }
            x += '</div><br>'
            QH('p30html3', x);

            // Update user's connection state
            x = '';
            if (activeSessions == 1) { x = "1 активна сесія"; } else if (activeSessions > 1) { x = format("{0} активних сеансів", 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>' + "Номер телефону цього користувача з можливістю відправки SMS." + '<br />' + "Залиште поле порожнім";
            x += '<br /><br /><div style=width:100%;text-align:center>' + "Номер телефону:" + ' <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', "Телефонні Сповіщення", 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%>' + "Месенджер";
            var y = '<select id=d2serviceselect style=width:160px;margin-left:8px onchange=p30editMessagingValidate()><option value=0>' + "Немає" + '</option>';
            if ((serverinfo.userMsgProviders & 1) != 0) { y += '<option value=1>' + "Telegram" + '</option>'; }
            if ((serverinfo.userMsgProviders & 4) != 0) { y += '<option value=4>' + "Discord" + '</option>'; }
            if ((serverinfo.userMsgProviders & 8) != 0) { y += '<option value=8>' + "XMPP" + '</option>'; }
            if ((serverinfo.userMsgProviders & 16) != 0) { y += '<option value=16>' + "CallMeBot" + '</option>'; }
            if ((serverinfo.userMsgProviders & 32) != 0) { y += '<option value=32>' + "Pushover" + '</option>'; }
            if ((serverinfo.userMsgProviders & 64) != 0) { y += '<option value=64>' + "ntfy" + '</option>'; }
            if ((serverinfo.userMsgProviders & 128) != 0) { y += '<option value=128>' + "Zulip" + '</option>'; }
            if ((serverinfo.userMsgProviders & 256) != 0) { y += '<option value=256>' + "Slack" + '</option>'; }
            y += '</select>';
            x += '<table style=margin-top:12px><tr><td>' + "Сервіс" + '<td>' + y;
            x += '<tr><td>' + "Обробка" + '<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">' + "японська" + '</a></div>' }
            x += '<div id=d2callmebotinfo style=display:none><br /><a href=https://www.callmebot.com/blog/free-api-signal-send-messages/ target="_callmebot">' + "Signal" + '</a>, <a href=https://www.callmebot.com/blog/free-api-whatsapp-messages/ target="_callmebot">' + "WhatsApp" + '</a>, <a href=https://www.callmebot.com/blog/free-api-facebook-messenger/ target="_callmebot">' + "Facebook" + '</a>, <a href=https://www.callmebot.com/blog/telegram-text-messages/ target="_callmebot">' + "Telegram" + '</a></div>';
            x += '<div id=d2pushoverinfo style=display:none><br /><a href=https://pushover.net/ target="_pushover">' + "Інформація про Pushover.net" + '</a></div>';
            x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Безкоштовний сервіс від ntfy.sh" + '</a></div>';
            x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Інсталяція Slack Webhook" + '</a></div>';
            xxdialogButtons = 3; xxdialogTag = 'verifyMessaging';
            setModalContent('xxAddAgent', "Сповіщення месенджера", 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'] = "Username:0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "ім'я_користувача@server.ua"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Ключ користувача"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Тема"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "ім'я_користувача@server.ua"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Ім'я користувача"; }
            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' : '') + '>' + "Запис сеансу" + '</label><br></div>';
            }
            x += '<div><label><input type=checkbox id=d20flag7 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000008) ? 'checked' : '') + '>' + "Відключити віддалене керування" + '</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' : '') + '>' + "Відключити доступ до стільниці" + '</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' : '') + '>' + "Лише перегляд екрану" + '</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' : '') + '>' + "Відключити доступ до терміналу" + '</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' : '') + '>' + "Відключити доступ до файлів" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag6 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000010) ? 'checked' : '') + '>' + "Відключити консоль агента" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag8 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00008000) ? 'checked' : '') + '>' + "Без доступу до деінсталяції агента" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag9 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00020000) ? 'checked' : '') + '>' + "Без віддалених команд" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag10 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000040) ? 'checked' : '') + '>' + "Без доступу до пробудження" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag11 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00040000) ? 'checked' : '') + '>' + "Без доступу до перезапуску/вимкнення" + '</label><br></div>';

            setModalContent('xxAddAgent', "Редагування можливостей користувача", 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("Змінити справжнє ім'я для {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("Стан", '<select id=dp30verified class="form-control" onchange=p30validateEmail()><option value=0>' + "Не веріфіковано" + '</option><option value=1>' + "Верифіковано" + '</option></select>'); }

            setModalContent('xxAddAgent', format("Змінити е-пошту для {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', "Попередні спроби входу", '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("Вимоги: {0}.", r.join(', ')) + '</div>'; }
            }

            x += '<div><label><input id=p4resetNextLogin class="form-check-input me-2" type=checkbox />' + "Примусово скинути пароль під час наступного входу." + '</label></div>';
            if (multiFactor == 1) { x += '<div><label><input id=p4twoFactorRemove class="form-check-input me-2" type=checkbox />' + "Видалити всі двофакторні автентифікації." + '</label></div>'; }
            setModalContent('xxAddAgent', format("Змінити пароль для {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("Видалити Користувача {0}", EscapeHtml(currentUser.name)), format("Підтвердити видалення користувача {0}?", EscapeHtml(currentUser.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Підтвердити" + '</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> ' + "Додати Групу Пристроїв" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Пов'язані Групи Пристроїв" + '</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>' + "Невідома Група Пристроїв" + '</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="' + "Видалити права користувача для цієї групи пристроїв" + '" 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="' + "Група пристроїв" + '" 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>' + "Немає пов'язаних груп пристроїв" + '</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> ' + "Додати Групу Користувачів" + '</button>'; }
                }
                x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Належність до груп користувачів" + '</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>' + "Невідома група користувачів" + '</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="' + "Видалити приналежність до групи користувачів" + '" 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="' + "Група користувачів" + '" 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>' + "Не належить до групи користувачів" + '</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> ' + "Додати Пристрій" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Пов'язані Пристрої" + '</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>' + "Невідомий Пристрій" + '</i>');
                    if ((cr & 2) != 0) {
                        trash = '<a href=# onclick=\'return p30removeNodeFromUser(event,"' + encodeURIComponentEx(node._id) + '")\' title="' + "Видалити права користувача для цієї групи пристроїв" + '" 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="' + "Пристрій" + '" 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>' + "Немає пов'язаних пристроїв" + '</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', "Видалити поширення до пристрою", format("Підтвердити скасування поширення пристрою \"{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', "Видалити дозволи пристрою", format("Підтвердити видалення прав доступу для пристрою \"{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', "Видалити дозволи користувача", name ? format("Підтвердити видалення прав доступу для користувача \"{0}\"?", name) : "Підтвердити видалення прав доступу?");
                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', "Видалити дозволи групи користувачів", name ? format("Підтвердити видалення прав доступу для групи користувачів \"{0}\"?", name) : "Підтвердити видалення прав доступу?");
                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>' + "Невідомо" + '</i>');
            setModalContent('xxAddAgent', "Видалити належність до групи користувачів", format("Підтвердити видалення приналежності до групи користувачів \"{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("Група користувачів", '<select id=dp2groupid class="form-select">' + y + '</select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Додати Приналежність", 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', "Видалити дозволи групи пристроїв", format("Підтвердити видалення прав доступу для групи пристроїв \"{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="' + "Це гостьовий сеанс спільного доступу" + '">' + 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="' + "Це гостьовий сеанс спільного доступу" + '">' + 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>' + "Подій не знайдено" + '</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>' + "Завантаження..." + '</i></div>';
            } else if (typeof p52recordings == 'number') {
                if (p52recordings == 1) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Сервер не може читати з теки записів." + '</i></div>'; }
                else if (p52recordings == 2) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Сервер не може отримати записи з бази даних." + '</i></div>'; }
                else { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Сталася невідома помилка." + '</i></div>'; }
            } else if (p52recordings.length == 0) {
                x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Немає записів." + '</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>' + "Сеанс" + '</th><th style=width:110px>' + nobreak("Час запуску") + '</th><th style=width:110px>' + "Тривалість" + '</th><th style=width:110px>' + "Розмір" + '</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} кБ", Math.round(rec.size / 1024)); }
            var sessionName = '<i>' + "Невідомо" + '</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 += ' - ' + "Термінальна сесія"; }
            if (rec.protocol == 2) { sessionName += ' - ' + "Cесія Стільниці"; }
            if (rec.protocol == 5) { sessionName += ' - ' + "Трансфер файлів"; }
            if (rec.protocol == 6) { sessionName += ' - ' + "PowerShell від імені адміністратора"; }
            if (rec.protocol == 8) { sessionName += ' - ' + "Консоль користувача"; }
            if (rec.protocol == 9) { sessionName += ' - ' + "PowerShell користувача"; }
            if (rec.protocol == 100) { sessionName += ' - ' + "Intel&reg; AMT WSMAN"; }
            if (rec.protocol == 101) { sessionName += ' - ' + "Перенаправлення Intel&reg; AMT"; }
            if (rec.protocol == 200) { sessionName += ' - ' + "Месенджер"; }

            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 = "Невідомо";
                if (rec.protocol == 1) { protocolStr = "Термінал"; }
                if (rec.protocol == 2) { protocolStr = "Стільниця"; }
                if (rec.protocol == 5) { protocolStr = "Файли"; }
                if (rec.protocol == 6) { protocolStr = "PowerShell від імені адміністратора"; }
                if (rec.protocol == 8) { protocolStr = "Консоль користувача"; }
                if (rec.protocol == 9) { protocolStr = "PowerShell користувача"; }
                if (rec.protocol == 100) { protocolStr = "Intel&reg; AMT WSMAN"; }
                if (rec.protocol == 101) { protocolStr = "Перенаправлення Intel&reg; AMT"; }
                if (rec.protocol == 200) { protocolStr = "Месенджер"; }
                x += addHtmlValue4("Протокол", protocolStr);
            }
            x += addHtmlValue4("Стан", (rec.present == 1) ? "Присутній на сервері" : "Не на сервері");
            if (rec.name) { x += addHtmlValue4("Назва Пристрою", EscapeHtml(rec.name)); }
            if (rec.meshname) { x += addHtmlValue4("Група пристроїв", EscapeHtml(rec.meshname)); }
            if (rec.size) { x += addHtmlValue4("Розмір", format("{0} байт", rec.size)); }
            if (rec.startTime) { x += addHtmlValue4("Час запуску", printTime(new Date(rec.startTime))); }
            if (rec.startTime && rec.lengthTime) { x += addHtmlValue4("Час закінчення", printTime(new Date(rec.startTime + (rec.lengthTime * 1000)))); }
            if (rec.lengthTime) { x += addHtmlValue4("Тривалість", 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("Мультиплексор", "Увімкнено"); }
            if (rec.userids) { for (var i in rec.userids) { x += addHtmlValue4("Користувач", rec.userids[i].split('/')[2]); } }

            xxdialogButtons = 9;
            setModalContent('xxAddAgent', "Деталі запису", 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: "Віддалені Сеанси", 2: "Використання Трафіку Користувачами", 3: "Лоґіни Користувача" }
            if (userinfo.siteadmin == 0xFFFFFFFF) { options[4] = "Записи Бази Даних"; }
            for (var i in options) { y += '<option value=' + i + ((settings.type == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Тип", '<select id=d2reportType class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');

            x += '<div id=d2GroupByDiv style=display:none>';
            y = '';
            var options = { 1: "Користувач", 2: "Пристрій", 3: "День" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Групувати як", '<select id=d2groupBy class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2GroupByDiv2 style=display:none>';
            y = '';
            var options = { 1: "Користувач", 3: "День" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy2 == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Групувати як", '<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' : '') + '>' + "Усі" + '</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("Група пристроїв", '<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: "Останній День", 7: "Останні 7 днів", 30: "Останні 30 днів", 0: "Діапазон часу" }
            for (var i in options) { y += '<option value=' + i + ((settings.timeRange == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Період", '<select id=d2timeRange class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2timeRangeDiv style=display:none>';
            x += addHtmlFormFloating("Початок діапазону часу", '<input id=d2timeRangeStartSelector class="flatpickr form-control" type="text" placeholder="' + "Оберіть Дату та Час..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Кінець діапазону часу", '<input id=d2timeRangeEndSelector class="flatpickr form-control" type="text" placeholder="' + "Оберіть Дату та Час..." + '" 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' : '') + '>' + "Показати трафік" + '</label><div>');
            x += '</div>';

            setModalContent('xxAddAgent', "Згенерувати звіт", 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: "Період", device: "Пристрій", session: "Сеанс", user: "Користувач", devgroup: "Група пристроїв", guest: "Гость", length: "Довжина", bytesin: "Байт Вх.", bytesout: "Байт Вих.", os: "Операційна система", ip: "IP-адреса", browser: "Браузер", msg: "Повідомлення", twofactor: "Двофакторний" }
            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>' + "Звіт не повернув жодних записів." + '</tr></td>';
            } else {
                reportCSV = '"' + "Група" + '"';
                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 "Термінал";
                if (v == 2) return "Стільниця";
                if (v == 5) return "Файли";
                if (v == 6) return "PowerShell від імені адміністратора";
                if (v == 8) return "Консоль користувача";
                if (v == 9) return "PowerShell користувача";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "Месенджер";
                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 "Невідомо" + ' (' + 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>' + "Невідомо" + '</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>' + "Невідомо" + '</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>' + "Невідомо" + '</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>' + "Невідомо" + '</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>' + "Невідомий користувач" + '</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>' + "Вхід успішний";
                if (v == 108) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny2"></div>' + "Помилковий 2FA код";
                if (v == 109) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny4"></div>' + "Заблокований акаунт";
                if (v == 110) return '<div style=float:left;margin-right:4px;cursor:pointer><i class="fa-solid fa-exclamation-triangle text-warning"></i></div>' + "Хибна спроба входу";
            }
            if (f == '2fa') {
                if (v == '') { return "Немає"; }
                if (v == 'backup') { return "Резервні Коди"; }
                if (v == 'fido') { return "ключ FIDO"; }
                if (v == 'sms') { return "SMS-повідомлення"; }
                if (v == 'hwotp') { return "Апаратний OTP"; }
                if (v == 'messenger') { return "Обмін повідомленнями"; }
                if (v == 'push') { return "Push-повідомлення"; }
                if (v == 'otp') { return "Одноразовий пароль (OTP)"; }
                if (v == 'cookie') { return "Запам'ятати пристрій"; }
                if (v == 'tokenlogin') { return "Токен Лоґіну"; }
                if (v == 'ipaddr') { return "IP-адреса"; }
                if (v == 'sso') { return "Єдиний вхід (SSO)"; }
            }
            if (f == 'records') {
                var c = {
                    'node': "Запис пристрою",
                    'mesh': "Запис групи пристроїв",
                    'user': "Записи користувача",
                    'sysinfo': "Записи інформації про пристрій",
                    'iploc': "Інформаційні записи про локацію IP",
                    'note': "Записи текстових приміток",
                    'ifinfo': "Записати інформацію про мережевий інтерфейс",
                    'cfile': "Записи файлу конфігурації",
                    'lastconnect': "Записи часу останнього підключення",
                    'power': "Записи про зміни стану живлення",
                    'events': "Записи подій",
                    'serverstats': "Записи статистики сервера",
                    'ugrp': "Записи групи користувачів",
                    'deviceshare': "Записи поширеного пристрою",
                    'logintoken': "Записи токенів входу до акаунту",
                    'smbios': "SMBIOS запис пристрою",
                    'pmt': "Запис Push-Сповіщень Пристрою",
                    'meshcentral': "Пристрій, користувачі, групи та інші записи"
                }
                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 "Термінал";
                if (v == 2) return "Стільниця";
                if (v == 5) return "Файли";
                if (v == 6) return "PowerShell від імені адміністратора";
                if (v == 8) return "Консоль користувача";
                if (v == 9) return "PowerShell користувача";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "Месенджер";
                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 "Невідомо" + ' (' + 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 "Невідомий користувач";
                }
            }
            if (f == 'msg') {
                if (v == 107) return "Вхід успішний";
                if (v == 108) return "Помилковий 2FA код";
                if (v == 109) return "Заблокований акаунт";
                if (v == 110) return "Хибна спроба входу";
            }
            if (f == 'records') { return renderReportFormat(v, f); }
            return '' + v;
        }

        function p60downloadReport() {
            if (xxdialogMode || (reportCSV == null)) return;
            saveAs(stringToUtf8Blob(reportCSV), "Report.csv");
        }

        //
        // NOTIFICATIONS
        //

        var notifications = [];

        // Toggle showing notifications
        function clickNotificationIcon(show) {
            //addNotification({ icon:0, text:'test' });
            if (show == true) { QV('notifiyBox', true); } else if (show == false) { QV('notifiyBox', false); } else { QV('notifiyBox', QS('notifiyBox')['display'] == 'none'); }
            drawNotifications();
        }

        // Set the notification count on the upper right of the screen
        function setNotificationCount(c) {
            var badge = Q('notificationBadge');
            if (parseInt(badge.innerHTML) === c) return;
            badge.innerHTML = c;
            QV('notificationCount', c > 0);
        }

        // Close notification box when clicking outside
        function setupNotificationClickOutside() {
            document.addEventListener('click', function(event) {
                var notificationBox = Q('notifiyBox');
                var notificationIcon = Q('notificationCount');
                if (notificationBox && QS('notifiyBox')['display'] !== 'none') {
                    if (!notificationBox.contains(event.target) && !notificationIcon.contains(event.target)) {
                        QV('notifiyBox', false);
                    }
                }
            });
        }
        setupNotificationClickOutside();


        // Refresh the notification box
        function drawNotifications() {
            var notifySettings = getstore('notifications', 0);
            var r = '';
            if (notifications.length == 0) {
                r = '<div style=margin:5px>' + "Наразі немає сповіщень" + '</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("Це сталося в {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="' + "Очистити це сповіщення" + '" 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">' + "Очистити все" + '</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,
                "Новий Акаунт", // 1
                "Ліміт сервера",
                "Попередження системи безпеки",
                "Налаштування Акаунту",
                "Група пристроїв",
                "Коди запрошень"
            ];
            var translatedMessages = [
                null,
                "У дозволі відмовлено", // 1
                "Хибне ім'я користувача",
                "Хибний пароль",
                "Хибна е-пошта",
                "Хибний домен",
                "Хибні дозволи сайту",
                "Користувач вже існує",
                "Неможливо додати користувача в цьому режимі",
                "Валідний виняток",
                "Досягнуто обмеження акаунту.", // 10
                "Запит на чат, натисніть тут, щоб прийняти",
                "Після останнього входу в акаунт кількість спроб невдалого входу дорівнює: {0}",
                "Не вдалося змінити адресу е-пошти, інший акаунт її вже використовує: {0}.",
                "Е-пошту надіслано.",
                "Користувача {0} не знайдено.",
                "Користувачів {0} не знайдено.",
                "Помилка, неможливо змінити на раніше використаний пароль.",
                "Помилка, неможливо змінити пароль на широко використовуваний.",
                "Помилка, пароль не змінено.",
                "Пароль змінено.", // 20
                "Поточний пароль неправильний.",
                "Помилка, код запрошення \"{0}\" уже використовується.",
                "Шлюз SMS вимкнено",
                "Немає прав керування користувачами",
                "Хибне SMS повідомлення",
                "Немає номера телефону для цього користувача",
                "SMS успішно надіслано.",
                "Помилка SMS",
                "Помилка SMS: {0}",
                "Домен е-пошти \"{0}\" заборонено. Дозволено лише ({1}).", // 30,
                "Хибне повідомлення",
                "Повідомлення:",
                "Помилка повідомлення",
                "Помилка повідомлення: {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: ["Використаний", "Вільно"] },
                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: ["Використаний", "Вільно"] },
                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: "Стан сервера",
                AgentErrorCounters: "Лічильник Помилок Агента",
                UnknownGroup: "Невідома Група",
                InvalidPKCSsignature: "Хибний підпис PKCS",
                InvalidRSAsignature: "Хибний підпис RSA",
                InvalidJSON: "Хибний JSON",
                UnknownAction: "Невідома Дія",
                BadWebCertificate: "Недійсний Вебсертифікат",
                BadSignature: "Помилковий Підпис",
                MaxSessionsReached: "Досягнуто Максимум Сеансів",
                UnknownDeviceGroup: "Невідома Група Пристроїв",
                InvalidDeviceGroupType: "Хибний Тип Групи Пристроїв",
                DuplicateAgent: "Дублювати Агента",
                ConnectedIntelAMT: "Підключено Intel&reg; AMT",
                ConnectedIntelAMTCira: "Підключено Intel&reg; AMT CIRA",
                RelayErrors: "Помилки Ретрансляції",
                UserAccounts: "Акаунти Користувачів",
                DeviceGroups: "Групи Пристроїв",
                AgentSessions: "Сесії Агента",
                ConnectedUsers: "Підключені Користувачі",
                UsersSessions: "Сеанси Користувачів",
                RelaySessions: "Сеанси Ретрансляції",
                RelayCount: "Лічильник Ретрансляцій"
            };

            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>' + "Навантаження ЦП" + '</div><div><b title="' + "Навантаження ЦП за останню хвилину" + '">' + (Math.round(message.cpuavg[0] * 100.0) / 100.0) + '</b>, <b title="' + "Навантаження ЦП за останні 5 хвилин" + '">' + (Math.round(message.cpuavg[1] * 100.0) / 100.0) + '</b>, <b title="' + "Навантаження ЦП за останні 15 хвилин" + '">' + (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>' + "Доступна пам'ять" + '</div><div><b>' + getNiceSize3(message.availablemem) + '</b> ' + "вільно" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "усього" + '</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>' + "Пам'ять" + '</div><div><b>' + getNiceSize3(message.freemem) + '</b> ' + "вільно" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "усього" + '</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 = ["Агент", "CIRA", "AMT-OS", "HTTP", "Ретрансляція", "Термінал", "Стільниця", "Файли", "WebRDP", "WebSSH", "WebVNC", "Мультиплексна Стільниця"];
        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 = "Кількість Підключень";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Агент", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Користувачі", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Сеанси Користувачів", data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: "Сеанси Ретрансляції", 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 = "Мегабайти";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Зовнішня", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Використана купа(Heap)", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Загальна купа(Heap)", 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 = "Мегабайти";
                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 = "Споживання";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "ЦП", 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' : '') + '>' + "Null" + '</option>'; }
                QH('p40server', x);
            }
            QV('p40server', servers.length > 0);
        }

        function p40downloadEvents() {
            var csv = "час, 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', "Подія трасування сервера", '<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>' + "Центральний Сервер" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c1 ' + ((serverTraceSources.indexOf('cookie') >= 0) ? 'checked' : '') + '>' + "Cookie кодувальник" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c2 ' + ((serverTraceSources.indexOf('dispatch') >= 0) ? 'checked' : '') + '>' + "Диспетчер Повідомлень" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c3 ' + ((serverTraceSources.indexOf('main') >= 0) ? 'checked' : '') + '>' + "Повідомлення Головного Сервера" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c4 ' + ((serverTraceSources.indexOf('peer') >= 0) ? 'checked' : '') + '>' + "Піринг сервера MeshCentral" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c15 ' + ((serverTraceSources.indexOf('agent') >= 0) ? 'checked' : '') + '>' + "Трафік MeshAgent" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c14 ' + ((serverTraceSources.indexOf('agentupdate') >= 0) ? 'checked' : '') + '>' + "Оновлення MeshAgent" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c16 ' + ((serverTraceSources.indexOf('cert') >= 0) ? 'checked' : '') + '>' + "Сертифікат Сервера" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c17 ' + ((serverTraceSources.indexOf('db') >= 0) ? 'checked' : '') + '>' + "База даних сервера" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c18 ' + ((serverTraceSources.indexOf('email') >= 0) ? 'checked' : '') + '>' + "е-пошта/SMS/Push обмін" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Вебсервер" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c5 ' + ((serverTraceSources.indexOf('web') >= 0) ? 'checked' : '') + '>' + "Вебсервер" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c6 ' + ((serverTraceSources.indexOf('webrequest') >= 0) ? 'checked' : '') + '>' + "Запити вебсервера" + '</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' : '') + '>' + "HTTP-заголовки вебсервера" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c21 ' + ((serverTraceSources.indexOf('authlog') >= 0) ? 'checked' : '') + '>' + "Журнал Автентифікації Користувача" + '</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' : '') + '>' + "Менеджер Intel AMT" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c9 ' + ((serverTraceSources.indexOf('webrelay') >= 0) ? 'checked' : '') + '>' + "Ретрансляція Підключення" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c10 ' + ((serverTraceSources.indexOf('mps') >= 0) ? 'checked' : '') + '>' + "Сервер CIRA" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c11 ' + ((serverTraceSources.indexOf('mpscmd') >= 0) ? 'checked' : '') + '>' + "Команди Сервера 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', "Моніторинг сервера", 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 = "час, джерело, повідомлення" + '\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, "Безпека акаунту", 1, null, "Неможливо отримати доступ до цієї властивості, доки не ввімкнено двофакторну автентифікацію. Це потрібно для додаткової безпеки. Перейдіть до \"Мій акаунт\" і перегляньте розділ \"Безпека акаунту\"."); 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', "Безпека акаунту", '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="' + "Версія несумісна, спочатку оновіть встановлення 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="' + "Версія несумісна, спочатку оновіть встановлення 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 = "Перевірка...";
                    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="' + "Переглянути Журнал Змін" + '" rel="noreferrer noopener" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
                        } else {
                            cant_action.push('upgrade');
                            if (p.status) p.upgradeAvail = "Оновлено";
                            else p.upgradeAvail = '<a title="' + "Переглянути Журнал Змін" + '" 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', "Дія Плаґіна", format("Ви впевнені, що хочете {0} плаґін: {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;">' + "Дозволи плагіна" + ' - ' + 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> ' + "Завантаження дозволів" + '...</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;">' + "Каскад дозволів:" + ' Node → Mesh → Global → Default' + '</div>';
            x += '<div>';
            x += '<button type="button" class="btn btn-secondary" style="margin-right:10px;" onclick="closePluginPermModal()">' + "Скасувати" + '</button>';
            x += '<button type="button" class="btn btn-primary" onclick="savePluginPermissionsEx()">' + "Зберегти" + '</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">' + "Цей плагін не визначив жодних дозволів." + '</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\')">' + "Глобальний" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary me-1" id="btnTabMeshes" onclick="switchPermTab(\'meshes\')">' + "Групи" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary" id="btnTabNodes" onclick="switchPermTab(\'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' : '') + '>✓ ' + "Дозволено" + '</option>';
                x += '<option value="denied"' + (defaultVal === 'denied' ? ' selected' : '') + '>✗ ' + "Відмовлено" + '</option>';
                x += '<option value="inherited"' + (defaultVal === 'inherited' ? ' selected' : '') + '>↩ ' + "Успадковано" + '</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">' + "Дозволено" + '</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">' + "Відмовлено" + '</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="' + "Пошук користувачів, груп, пристроїв" + '..." ';
            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' ? "Виберіть групу пристроїв" : "Виберіть пристрій" ) + ' --</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' ? "Спеціальні дозволи для цієї групи пристроїв" : "Спеціальні дозволи для цього пристрою") + '</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("Помилка Виклику", 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>