<!DOCTYPE html><html lang="fi" 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>Tiedot</b></div>
        <div id="cxdesktop" class="cmtext" onclick="cmaction(3,event)">Työpöytä</div>
        <div id="cxterminal" class="cmtext" onclick="cmaction(2,event)">Pääte</div>
        <div id="cxfiles" class="cmtext" onclick="cmaction(4,event)">Tiedostot</div>
        <div id="cxevents" class="cmtext" onclick="cmaction(5,event)">Tapahtumat</div>
        <div id="cxdetails" class="cmtext" onclick="cmaction(6,event)">Tarkemmat tiedot</div>
        <div id="cxconsole" class="cmtext" onclick="cmaction(7,event)">Konsoli</div>
        <div id="cxwebrdp" class="cmtext" onclick="cmaction(11,event)">Verkko-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">Liitännäiset</div>
        <hr id="cxmgroupsplit">
        <div id="cxstar" class="cmtext" onclick="cmaction(10,event)" style="display:none">Vaihda tähti</div>
        <div id="cxmdesktop" class="cmtext" onclick="cmaction(9,event)" style="display:none">Monipöytä</div>
    </div>
    <div id="meshContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div id="cxselectall" class="cmtext" onclick="cmmeshaction(1,event)">Valitse kaikki</div>
        <div id="cxselectnone" class="cmtext" onclick="cmmeshaction(2,event)">Poista valinta</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)">Vaihtoehtoinen portti</div>
    </div>
    <div id="sshPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmsshportaction(1,event)">Vaihtoehtoinen portti</div>
    </div>
    <div id="rfbPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmrfbportaction(1,event)">Vaihtoehtoinen portti</div>
    </div>
    <div id="httpPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpportaction(1,event)">Vaihtoehtoinen portti</div>
    </div>
    <div id="httpsPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpsportaction(1,event)">Vaihtoehtoinen portti</div>
    </div>
    <div id="filesContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmfilesaction(1,event)">Nimeä uudelleen</div>
        <div class="cmtext" onclick="cmfilesaction(2,event)">Muokkaa</div>
        <div class="cmtext" onclick="cmfilesaction(3,event)">Poista</div>
    </div>
    <div id="expandAllContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmexpandaction(1,event)">Laajenna kaikki</div>
        <div class="cmtext" onclick="cmexpandaction(2,event)">Kutista kaikki</div>
    </div>
    <div id="deskPlayerContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskplayeraction(1,event)">Avaa pelaaja ...</div>
    </div>
    <div id="deskKeyShortcutContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskshortcutaction(1,event)">Mukauta ...</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)">Mukauta ...</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="Napsauta nähdäksesi nykyiset ilmoitukset" 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>Omat laitteet</span>
                                </div>

                                <div class="userDropdownMenuItem userDropdownMobileOnly mobile-menu-item" data-action="events" onclick="handleUserMenuItem('events')">
                                    <i class="fa fa-calendar userDropdownMenuIcon"></i>
                                    <span>Omat Tapahtumat</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>Omat käyttäjät</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>Omat Tiedostot</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>Oma Palvelin</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>Käyttöliittymän asetukset</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>Vaihda moderni käyttöliittymä</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>Vasen palkin käyttöliittymä</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="top-bar" onclick="handleUserMenuItem('top-bar')">
                                        <i class="fa fa-window-maximize userDropdownMenuIcon"></i>
                                        <span>Yläpalkin käyttöliittymä</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="fixed-width" onclick="handleUserMenuItem('fixed-width')">
                                        <i class="fa fa-columns userDropdownMenuIcon"></i>
                                        <span>Kiinteä leveyksinen käyttöliittymä</span>
                                    </div>
                                    <div id="toggleFooterMenuItem" class="userDropdownMenuItem" data-action="toggle-footer" onclick="handleUserMenuItem('toggle-footer')">
                                        <i class="fa fa-window-minimize userDropdownMenuIcon"></i>
                                        <span>Vaihda alatunnistepalkki</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">Vaihda yötilaa</span>
                                </div>


                                <div class="userDropdownMenuItem" data-action="notes" onclick="handleUserMenuItem('notes')">
                                    <i class="fa fa-sticky-note userDropdownMenuIcon"></i>
                                    <span>Henkilökohtaisia ​​muistiinpanoja</span>
                                </div>

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

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

                                <div class="userDropdownMenuItem" data-action="logout" onclick="handleUserMenuItem('logout')">
                                    <i class="fa fa-sign-out userDropdownMenuIcon"></i>
                                    <span>Kirjaudu ulos</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)">Omat laitteet</td>
                            <td tabindex="0" id="MainMenuMyAccount" class="topbar_td style3x" onmouseup="go(2,event)" onkeypress="if (event.key == 'Enter') go(2)">Oma Tili</td>
                            <td tabindex="0" id="MainMenuMyEvents" class="topbar_td style3x" onmouseup="go(3,event)" onkeypress="if (event.key == 'Enter') go(3)">Omat Tapahtumat</td>
                            <td tabindex="0" id="MainMenuMyFiles" class="topbar_td style3x" onmouseup="go(5,event)" onkeypress="if (event.key == 'Enter') go(5)">Omat Tiedostot</td>
                            <td tabindex="0" id="MainMenuMyUsers" class="topbar_td style3x" onmouseup="go(4,event)" onkeypress="if (event.key == 'Enter') go(4)">Omat käyttäjät</td>
                            <td tabindex="0" id="MainMenuMyServer" class="topbar_td style3x" onmouseup="go(6,event)" onkeypress="if (event.key == 'Enter') go(6)">Oma Palvelin</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)">Yleistä</td>
                                        <td tabindex="0" id="MainDevDesktop" class="topbar_td style3x" onmouseup="go(11,event)" onkeypress="if (event.key == 'Enter') go(11)">Työpöytä</td>
                                        <td tabindex="0" id="MainDevTerminal" class="topbar_td style3x" onmouseup="go(12,event)" onkeypress="if (event.key == 'Enter') go(12)">Pääte</td>
                                        <td tabindex="0" id="MainDevFiles" class="topbar_td style3x" onmouseup="go(13,event)" onkeypress="if (event.key == 'Enter') go(13)">Tiedostot</td>
                                        <td tabindex="0" id="MainDevEvents" class="topbar_td style3x" onmouseup="go(16,event)" onkeypress="if (event.key == 'Enter') go(16)">Tapahtumat</td>
                                        <td tabindex="0" id="MainDevInfo" class="topbar_td style3x" onmouseup="go(17,event)" onkeypress="if (event.key == 'Enter') go(17)">Tarkemmat tiedot</td>
                                        <td tabindex="0" id="MainDevApps" class="topbar_td style3x" onmouseup="go(18,event)" onkeypress="if (event.key == 'Enter') go(18)">Ohjelmisto</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)">Konsoli</td>
                                        <td tabindex="0" id="MainDevPlugins" class="topbar_td style3x" onmouseup="go(19,event)" onkeypress="if (event.key == 'Enter') go(19)">Liitännäiset</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)">Yleistä</td>
                                <td tabindex="0" id="MeshSummary" class="topbar_td style3x" onmouseup="go(21,event)" onkeypress="if (event.key == 'Enter') go(21)">Yhteenveto</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)">Tapahtumat</td>
                                <td tabindex="0" id="EventsReport" class="topbar_td style3x" onmouseup="go(60,event)" onkeypress="if (event.key == 'Enter') go(60)">Raportit</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)">Yleistä</td>
                                <td tabindex="0" id="UserEvents" class="topbar_td style3x" onmouseup="go(31,event)" onkeypress="if (event.key == 'Enter') go(31)">Tapahtumat</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)">Käyttäjät</td>
                                <td tabindex="0" id="UsersGroups" class="topbar_td style3x" onmouseup="go(50,event)" onkeypress="if (event.key == 'Enter') go(50)">Ryhmät</td>
                                <td tabindex="0" id="UsersRecordings" class="topbar_td style3x" onmouseup="go(52,event)" onkeypress="if (event.key == 'Enter') go(52)">Nauhoitukset</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)">Yleistä</td>
                                <td tabindex="0" id="ServerStats" class="topbar_td style3x" onmouseup="go(40,event)" onkeypress="if (event.key == 'Enter') go(40)">Tilastot</td>
                                <td tabindex="0" id="ServerConsole" class="topbar_td style3x" onmouseup="go(115,event)" onkeypress="if (event.key == 'Enter') go(115)">Konsoli</td>
                                <td tabindex="0" id="ServerTrace" class="topbar_td style3x" onmouseup="go(41,event)" onkeypress="if (event.key == 'Enter') go(41)">Jäljitä</td>
                                <td tabindex="0" id="ServerPlugins" class="topbar_td style3x" onmouseup="go(42,event)" onkeypress="if (event.key == 'Enter') go(42)">Liitännäiset</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">Palvelinyhteys katkennut</span>, <href onclick="reload()" style="cursor:pointer"><u>napsauta yhdistääksesi uudelleen</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">Omat laitteet</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="Go to first page" 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="Go to previous page" 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="Go to next page" 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="Go to last page" 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="Sarakkeet">
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 right-4 up-2"></i>
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 left-4 up-2"></i>
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 right-4 down-3"></i>
                            <i class="fa-solid fa-bars" data-fa-transform="shrink-8 left-4 down-3"></i>
                        </span>
                        <span role="button" tabindex="0" id="devViewButton2" class="fa-layers fa-fw fa-xl" onclick="onDeviceViewChange(2)" onkeypress="if (event.key == 'Enter') { onDeviceViewChange(2); }" title="Lista">
                            <i class="fa-solid fa-bars"></i>
                        </span>
                        <span role="button" tabindex="0" id="devViewButton3" class="fa-layers fa-fw fa-xl" onclick="onDeviceViewChange(3)" onkeypress="if (event.key == 'Enter') { onDeviceViewChange(3); }" title="Työpöydät">
                            <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="Työpöydät">
                            <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="Kartta" 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="Kutista kaikki" 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="Laajenna kaikki" 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="Valitse kaikki">
                        <input type="button" class="btn btn-primary me-1 btn-sm" id="GroupActionButton" disabled="disabled" value="Ryhmätoiminta" onclick="groupActionFunction()">
                        <input type="button" class="btn btn-secondary me-1 btn-sm" id="ScrollToTopButton" onclick="onDevicesScroll(true);" value="Scroll To Top">
                        <input type="text" id="SearchInput" class="form-control-sm me-1 btn-sm" placeholder="Suodata" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Suodata laitteita nimen tai etuliitteen mukaan: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Käytä ! kieltämiseen, esim. !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="Laitesuodatin">
                            <option value="0" selected="">Kaikki</option>
                            <option value="1">Online-tilassa</option>
                            <option value="5">Offline-tilassa</option>
                            <option value="2">Istuntoja</option>
                            <option value="3">Tähdellä merkitty</option>
                            <option value="4">Intel® AMT</option>
                            <option value="6">auta</option>
                            <option value="7">Merkitty</option>
                            <option value="8">Tunnistamaton</option>
                        </select>
                        <div class="form-check" title="Näytä laitteiden käyttöjärjestelmän nimi"><input class="form-check-input me-1" type="checkbox" id="RealNameCheckBox" onclick="onRealNameCheckBox()"><label class="form-check-label" for="RealNameCheckBox">OS-nimi</label></div>
                        <label style="display:none"><input type="checkbox" id="OnlineCheckBox" class="form-check-input me-2" onclick="onOnlineCheckBox(event)"><span title="Näytä vain online-tilassa olevat laitteet">Online-tilassa</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="Kutista kaikki" 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="Laajenna kaikki" 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="Yhdistä kaikkiin">
                            <input type="button" class="btn btn-danger me-1 btn-sm" onclick="disconnectAllKvmFunction()" value="Katkaise kaikki yhteydet">
                            <div class="form-check form-check-inline me-1 align-middle" title="Automaattinen yhdistäminen">
                                <input class="form-check-input me-1" type="checkbox" id="autoConnectDesktopCheckbox" onclick="autoConnectDesktops(event)">
                                <label class="form-check-label" for="autoConnectDesktopCheckbox">Auto</label>
                            </div>
                        </span>
                        <input type="button" class="btn btn-secondary me-1 btn-sm" onclick="onDevicesScroll(true);" value="Scroll To Top">
                        <input type="button" class="btn btn-primary me-1 btn-sm" onclick="showMultiDesktopSettings()" value="Asetukset">
                        <input type="text" id="KvmSearchInput" class="form-control-sm me-1" placeholder="Suodata" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Suodata laitteita nimen tai etuliitteen mukaan: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Käytä ! kieltämiseen, esim. !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="Etsi sijainti" onfocus="onMapSearchFocus(1)" onblur="onMapSearchFocus(0)">
                        <input class="btn btn-primary me-1 btn-sm" type="button" value="Etsi" title="Etsi sijaintia" onclick="getSearchLocation()">
                        <input class="btn btn-secondary me-1 btn-sm" type="button" id="refreshmap" title="Nollaa kartta näkymä" value="Nollaa" onclick="refreshMap(false,true)">
                    </div>
                    <div class="d-flex align-items-center ms-auto">
                        <div style="display:none" id="devListToolbarView">
                            Näytä
                            <select id="viewselect" onchange="onDeviceViewChange()">
                                <option value="1">Sarakkeet</option>
                                <option value="2">Lista</option>
                                <option value="3">Pöytäkoneet Kiinteä leveys</option>
                                <option id="viewselectmapoption" value="4" style="display:none">Kartta</option>
                                <option value="5">Työpöydät</option>
                            </select>
                        </div>
                        <div style="display:none" id="devListToolbarSize">
                            Koko
                            <select id="sizeselect" class="form-select-sm me-2" onchange="onDeviceViewChange()">
                                <option value="0">Pieni</option>
                                <option value="1">Keskikokoinen</option>
                                <option value="2">Suuri</option>
                            </select>
                            &nbsp;
                        </div>
                        <div style="display:none" id="devListToolbarSort">
                            Lajittele
                            <select id="sortselect" class="form-select-sm me-2" onchange="mainUpdate(6)">
                                <option>Ryhmä</option>
                                <option>Virta</option>
                                <option>Laite</option>
                                <option>Tunnisteet</option>
                                <option>Ryhmätunnisteet</option>
                                <option>Viimeksi nähty</option>
                                <option>Last Boot Up Time</option>
                            </select>
                            &nbsp;
                        </div>
                        <i class="fa-solid fa-gear" role="button" id="devListToolbarSettings" onclick="onDeviceViewSettings()"></i>
                    </div>
                </div>
                <div id="NoMeshesPanel" style="display:none">
                    <table>
                        <tbody><tr>
                            <td valign="top" style="width:50px">
                                <img src="images/info.png" width="47" height="48">
                            </td>
                            <td>
                                <div id="getStarted1">Aloita <a href="#" onclick="return account_createMesh()"><strong>luo laiteryhmä napsauttamalla tätä</strong></a>.</div>
                                <div id="getStarted2">Ei laiteryhmiä.</div>
                            </td>
                        </tr>
                    </tbody></table>
                </div>
                <div id="xdevices" class="noselect" style="display:none" onscroll="onDevicesScroll(false)"></div>
                <div id="xdevicesmap" style="display:none">
                    <div id="xmapSearchResultsDlg" style="display:none">
                        <div id="xmapSearchResultsBck">
                            <div id="xmapSearchClose" onclick="mapCloseSearchWindow()"><b>X</b></div>
                            <div style="padding:5px">Sijaintitulokset</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>Oma Tili</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)">Vaihda kuva</a></div>
                    </div>
                    <div id="p2AccountSecurity" style="display:none">
                        <p><strong>Tilin turvallisuus</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()">Hallitse puhelinnumeroa</a><br></span>
                            </div>
                            <div id="manageEmail2FA">
                                <div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthEmail()">Hallinnoi sähköpostitodennusta</a><br></span>
                            </div>
                            <div id="manageAuthApp">
                                <div class="p2AccountActions"><span id="authAppSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthApp()">Hallinnoi todentajasovellusta</a><br></span>
                            </div>
                             <div id="manageDuoApp">
                                <div class="p2AccountActions"><span id="authDuoSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthDuo()">Hallinnoi Duo-todennusta</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)">Hallinnoi suojausavaimia</a><br></span>
                            </div>
                            <div id="managePushAuthDev">
                                <div class="p2AccountActions"><span id="authPushAuthDevCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_managePushAuthDev()">Hallitse push -todennusta</a><br></span>
                            </div>
                            <div id="manageMessaging1">
                                <div class="p2AccountActions"><span id="authMessagingCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageMessaging()">Manage messaging</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)">Hallinnoi varmuuskoodeja</a><br></span>
                            </div>
                            <div class="p2AccountActions"></div><span><a href="#" onclick="return account_viewPreviousLogins()">Näytä aiemmat kirjautumistiedot</a><br></span>
                        </div>
                    </div>
                    <div id="p2AccountActions">
                        <p><strong>Tilitoiminnot</strong></p>
                        <p class="mL">
                            <span id="managePhoneNumber2" style="display:none"><a href="#" onclick="return account_managePhone()">Hallitse puhelinnumeroa</a><br></span>
                            <span id="manageMessaging2" style="display:none"><a href="#" onclick="return account_manageMessaging()">Manage messaging</a><br></span>
                            <span id="verifyEmailId" style="display:none"><a href="#" onclick="return account_showVerifyEmail()">Vahvista sähköpostiosoite</a><br></span>
                            <span id="accountEnableNotificationsSpan" style="display:none"><a href="#" onclick="return account_enableNotifications()">Ota käyttöön web-ilmoitukset</a><br></span>
                            <a href="#" onclick="return account_showLocalizationSettings()">Lokalisointiasetukset</a><br>
                            <a href="#" onclick="return account_showAccountNotifySettings()">Ilmoitusasetukset</a><br>
                            <span id="p2AccountPassActions">
                                <span id="accountChangeEmailAddressSpan" style="display:none"><a href="#" onclick="return account_showChangeEmail()">Vaihda sähköpostiosoite</a><br></span>
                                <a href="#" onclick="return account_showChangePassword()">Vaihda salasana</a><span id="p2nextPasswordUpdateTime"></span><br>
                                <a href="#" onclick="return account_showDeleteAccount()">Poista tili</a><br>
                            </span>
                            <span id="accountCreateLoginTokenSpan" style="display:none"><a href="#" onclick="return account_createLoginToken()">Luo kirjautumistunnus</a><br></span>
                            <a href="#" onclick="return account_showThemesSwitcher()">Vaihda teema</a><br>
                            <span id="accountCustomIconsSpan" style="display:none"><a href="#" onclick="return showIconCustomization()">Kuvakkeiden mukauttaminen</a><br></span>
                        </p>
                    </div>
                    <div id="p2logintokens"></div>
                    <strong>Laiteryhmät</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> Uusi</button></span>
                    <br><br>
                    <div id="p2meshes"></div>
                    <div id="p2noMeshFound" style="display:none">Ei laiteryhmiä.<span id="p2createMeshLink2"> <a href="#" onclick="return account_createMesh()"><strong>Aloita täältä!</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">Omat Tapahtumat</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">Suodata</label>
                                <select id="p3filterevents" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="">All Logs</option>
                                    <option notransval="1" value="agentlog">Agent Logs</option>
                                    <option notransval="1" value="relaylog">Relay Logs</option>
                                    <option notransval="1" value="manual">Manual Logs</option>
                                    <option notransval="1" value="runcommands">Run Command Logs</option>
                                    <option notransval="1" value="batchupload">Batch Upload Logs</option>
                                    <option notransval="1" value="changenode">Change Node Logs</option>
                                    <option notransval="1" value="removenode">Remove Node Logs</option>
                                </select>
                                <label for="p3limitdropdown" class="me-1">Näytä</label>
                                <select id="p3limitdropdown" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="60">Viimeiset 60</option>
                                    <option notransval="1" value="120">Viimeiset 120</option>
                                    <option notransval="1" value="250">Viimeiset 250</option>
                                    <option notransval="1" value="500">Viimeiset 500</option>
                                    <option notransval="1" value="1000">Viimeiset 1000</option>
                                    <option notransval="1" value="">Ei rajoitusta</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(2)"><i class="fa-solid fa-download" title="Lataa tapahtumat"></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">Omat käyttäjät</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="Valitse kaikki">
                                <input type="button" id="UsersGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Ryhmätoiminta" onclick="p3usersGroupActionFunction()">
                                <input id="UserNewAccountButton" class="btn btn-success btn-sm me-1" type="button" onclick="showCreateNewAccountDialog()" value="Uusi tili...">
                                <div class="btn-group me-1 mobileActionDropdown">
                                    <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">Lisää&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
                                    <div class="dropdown-menu">
                                        <button type="button" class="dropdown-item" onclick="showUserBroadcastDialog()">Yleislähetys</button>
                                        <button type="button" class="dropdown-item" onclick="p4downloadUserInfo()">Lataa käyttäjän tiedot</button>
                                        <button type="button" id="p4MobileUserBatchCreate" class="dropdown-item" style="display:none" onclick="p4batchAccountCreate()">Luo tilejä eräajona</button>
                                        <button type="button" class="dropdown-item" onclick="onUsersViewSettings()">Asetukset</button>
                                    </div>
                                </div>
                                <div class="d-inline-flex align-items-center">
                                    <input id="UserSearchInput" type="search" class="form-control-sm me-1" placeholder="Suodata" 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="Yleislähetys">
                                <a href="#" onclick="p4downloadUserInfo()"><i role="button" class="fa-solid fa-download me-2" title="Lataa käyttäjän tiedot"></i></a>
                                <a href="#" onclick="p4batchAccountCreate()"><i role="button" id="p4UserBatchCreate" style="display:none" title="Erä luo useita käyttäjätilejä" 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="Asetukset" 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">Omat Tiedostot</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="Ylös">
                                <input type="button" id="p5SelectAllButton" class="btn btn-secondary btn-sm me-1" disabled="disabled" onclick="p5selectallfile();" value="Valitse kaikki">
                                <input type="button" id="p5RenameFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Nimeä uudelleen" onclick="p5renamefile();">
                                <input type="button" id="p5DeleteFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Poista" onclick="p5deletefile();">
                                <input type="button" id="p5ViewFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Muokkaa" onclick="p5viewfile()">
                                <input type="button" id="p5NewFolderButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Uusi kansio" onclick="p5createfolder();">
                                <input type="button" id="p5UploadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Lähetä" onclick="p5uploadFile()">
                                <input type="button" id="p5DownloadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Lataa" onclick="p5downloadButton()">
                                <input type="button" id="p5CutButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Leikkaa" onclick="p5copyFile(1)">
                                <input type="button" id="p5CopyButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Kopioi" onclick="p5copyFile(0)">
                                <input type="button" id="p5PasteButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Liitä" 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">Lisää&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()">Uusi kansio</button>
                                        <button type="button" disabled="disabled" id="p5MobileUploadButton" class="dropdown-item" onclick="p5uploadFile()">Lähetä</button>
                                        <button type="button" disabled="disabled" id="p5MobileDownloadButton" class="dropdown-item" onclick="p5downloadButton()">Lataa</button>
                                        <button type="button" disabled="disabled" id="p5MobileCutButton" class="dropdown-item" onclick="p5copyFile(1)">Leikkaa</button>
                                        <button type="button" disabled="disabled" id="p5MobileCopyButton" class="dropdown-item" onclick="p5copyFile(0)">Kopioi</button>
                                        <button type="button" disabled="disabled" id="p5MobilePasteButton" class="dropdown-item" onclick="p5pasteFile()">Liitä</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">Lajittele nimen mukaan</option>
                                    <option value="2">Lajittele koon mukaan</option>
                                    <option value="3">Lajittele päivämäärän mukaan</option>
                                    <option value="4">Laskeva nimen mukaan</option>
                                    <option value="5">Laskeva koon mukaan</option>
                                    <option value="6">Laskeva päivämäärän mukaan</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>Nämä tiedostot jaetaan julkisesti, napsauta "linkki" saadaksesi julkisen URL-osoitteen.</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">Oma Palvelin</div>
                        <img id="MainMeshImage" src="serverpic.ashx" class="ms-auto">
                    </div>
                    <div id="p2ServerActions">
                        <p><strong>Palvelimen toiminnot</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()">Lataa palvelimen varmuuskopio</a>
                            </div>
                            <div id="p2ServerActionsRestore">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showRestoreDlg()">Palauta palvelin varmuuskopiosta</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 Driven varmuuskopio</a><br></span>
                            </div>
                            <div id="p2ServerActionsVersion">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showVersionDlg()">Tarkista palvelimen versio</a>
                            </div>
                            <div id="p2ServerActionsErrors">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showErrorsDlg()">Näytä palvelimen virheloki</a>
                            </div>
                            <div id="p2ServerActionsConfig">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showConfigDlg()">Show server configuration</a>
                            </div>
                        </div>
                        <br>
                    </div>
                    <strong>Palvelintilastot</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>Palvelimen varoitukset</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="Takaisin" 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>Yleistä<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>Muistiinpanot</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="Tähdellä merkitty" 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="Luettelo tämän tietokoneen aktiivisista etäistunnoista." 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="Apua pyydetty" 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="Takaisin" 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>Työpöytä<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="Koko näyttö. Pidä vaihtonäppäintä selaimen koko näytössä."></i>
                    </div>
                </div>
                <div id="p11warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Uudelleenohjausportti tai KVM-ominaisuus on poistettu käytöstä<span id="p11warninga">, ota se käyttöön napsauttamalla tätä.</span></div>
                </div>
                <div id="p11warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Remote computer is not powered on, click here to issue a power command.
                    </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="Automaattinen yhdistäminen" 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="Yhdistä käyttämällä MeshAgent-etätyöpöytää" onclick="connectDesktop(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Yhdistä</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">Vaihda pudotusvalikko</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(1,event)">Pyydä Hyväksyntää + Ilmoitus</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(2,event)">Pyydä Hyväksyntää</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(3,event)">Tietosuojapalkki</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="Yhdistä RDP:n avulla" onclick="askRdpCredentials()" onkeypress="return false" onkeydown="return false" disabled="disabled">RDP-yhteys</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">Vaihda pudotusvalikko</span></button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmaltportaction(1,event)">Vaihtoehtoinen portti</a></li>
                                </ul>
                            </div>
                            <span id="connectbutton1hspan"><button class="btn btn-primary btn-sm me-1" type="button" id="connectbutton1h" title="Yhdistä laitteisto-KVM:llä" onclick="connectDesktop(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">HW Connect</button></span>
                            <div class="btn-group dropdown-center me-1" id="disconnectbutton1span">
                                <button type="button" class="btn btn-danger btn-sm" id="disconnectbutton1" title="Yhdistä käyttämällä MeshAgent-etätyöpöytää" onclick="connectDesktop(event,0)" onkeypress="return false" onkeydown="return false">Katkaise yhteys</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">Vaihda pudotusvalikko</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(10,event)">Irrota ja lukitse</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(11,event)">Katkaise yhteys</a></li>
                                </ul>
                            </div>
                            <span id="deskstatus" style="line-height:22px">Yhteys katkaistu</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="Vaihda etäkoneen virrantila" onkeypress="return false" onkeydown="return false" value="Virtatoiminnot..." onclick="showPowerActionDlg()" style="display:none">
                            <input id="deskActionsBtn" type="button" class="btn btn-primary btn-sm me-1" title="Virranhallintatoimintojen suorittaminen laitteessa" onkeypress="return false" onkeydown="return false" value="Toiminnot" onclick="deviceActionFunction()">
                            <input id="deskActionsSettings" type="button" class="btn btn-primary btn-sm me-1" value="Asetukset" title="Muokkaa etätyöpöydän asetuksia" onkeypress="return false" onkeydown="return false" onclick="showDesktopSettings()">
                            <input id="deskFocusBtn" type="button" title="Vaihda tarkennustila, kun aktiivinen vain hiiren ympärillä oleva alue päivitetään" onkeypress="return false" onkeydown="return false" value="Kohdista kaikki" onclick="deskToggleFocus()" style="margin-right:3px;display:none">
                            <div id="deskRecordIcon" class="deskareaicon" title="Palvelin tallentaa tätä istuntoa" style="display:none;background-color:red;width:12px;height:12px;border-radius:6px;margin-top:5px"></div>
                            <div class="deskareaicon" title="Kierrä oikealle" onclick="drotate(1)"><i class="fa-solid fa-rotate-right"></i></div>
                            <div class="deskareaicon" title="Käänny vasemmalle" onclick="drotate(-1)"><i class="fa-solid fa-rotate-left"></i></div>
                            <div class="deskareaicon" title="Vaihda näkymää" 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()">Päivitä</a>
                                <div id="deskToolsTopTabProcess" class="deskToolsTopTab" onclick="changeDeskToolTab(0)" style="left:0px;bottom:0px">Prosessit</div>
                                <div id="deskToolsTopTabService" class="deskToolsTopTab" onclick="changeDeskToolTab(1)" style="display:none;left:90px;color:gray">Palvelut</div>
                            </div>
                            <div id="deskToolsArea">
                                <div id="DeskToolsProcessTab">
                                    <div id="deskToolsHeader">
                                        <a class="colmn1" title="Lajittele prosessi tunnuksen mukaan" onclick="sortProcess(0)">PID</a>
                                        <a class="colmn2" title="Lajittele nimen mukaan" onclick="sortProcess(1)">Nimi</a>
                                    </div>
                                    <div id="DeskToolsProcesses" style=""></div>
                                </div>
                                <div id="DeskToolsServiceTab" style="display:none">
                                    <div id="deskToolsServiceHeader">
                                        <a class="colmn1" style="width:70px" title="Lajittele tilan mukaan" onclick="sortService(0)">Tila</a>
                                        <a class="colmn2" title="Lajittele nimen mukaan" onclick="sortService(1)">Nimi</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()">Lähetä</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="Leikepöytä" onkeypress="return false" onkeydown="return false" onclick="showDeskClip()">
                            <input id="DeskType" class="btn btn-secondary btn-sm me-1" cmenu="deskPreConfigShortcutContextMenu" type="button" value="Tyyppi" 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="Näytä/piilota näyttönäppäimistö" style="display:none">
                                <i class="fa-solid fa-keyboard"></i>
                            </button>
                            <div id="DeskControlSpan" title="Vaihda hiiren ja näppäimistön syötettä" 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">Syöte</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="Lisää toimintoja">
                                Lisää&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="Työpöytä istunnon latenssi"></span>
                            <span id="DeskTimer" style="width:70px" class="text-center" title="Istunnon kesto"></span>
                            <input id="DeskToolsButton" type="button" class="btn btn-primary btn-sm mx-1" value="Työkalut" title="Vaihda työkalujen näkymä" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()">
                            <span id="DeskGuestShareButton" title="Jaa laite vieraan kanssa" role="button">
                                <i class="fa-solid fa-fw fa-share-nodes" id="DeskInputUnLockedButtonImage" onclick="showShareDevice()"></i>
                            </span>
                            <span id="DeskInputUnLockedButton" title="Etätulon lukitus on avattu" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" onclick="deskInputLockFunction(1)"></i>
                            </span>
                            <span id="DeskInputLockedButton" title="Etätulo on lukittu" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" style="color:red" onclick="deskInputLockFunction(0)"></i>
                            </span>
                            <span id="DeskRefreshButton" title="Päivitä työpöytä" role="button">
                                <i class="fa-solid fa-fw fa-rotate" onclick="deskRefreshFunction()"></i>
                            </span>
                            <span id="DeskClipboardOutButton" title="Lähetä paikallinen leikepöytä etälaitteeseen" 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="Lataa etäleikepöytä paikalliselle leikepöydälle" 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="Etätyöpöytäistunnon tallentaminen tiedostoon" style="display:none" role="button">
                                <i class="fa-solid fa-fw fa-video" onclick="deskRecordSession()" id="DeskRecordButtonImage"></i>
                            </span>
                            <span id="DeskSaveImageButton" title="Tallenna kuvakaappaus etätyöpöydältä" role="button">
                                <i class="fa-solid fa-fw fa-camera" onclick="deskSaveImage()"></i>
                            </span>
                            <span id="DeskBackgroundButton" title="Vaihda etätyöpöydän tausta" role="button">
                                <i class="fa-solid fa-fw fa-image" onclick="deviceToggleBackground(event)" id="DeskBackgroundButtonImage"></i>
                            </span>
                            <span id="DeskOpenWebButton" title="Avaa Web-osoite etätietokoneella" role="button">
                                <i class="fa-solid fa-fw fa-globe" onclick="deviceUrlFunction()"></i>
                            </span>
                            <span id="DeskLockButton" title="Lukitse etätietokone" role="button">
                                <i class="fa-solid fa-fw fa-lock" onclick="deviceLockFunction()"></i>
                            </span>
                            <span id="DeskNotifyButton" title="Näytä ilmoitus etätietokoneella" role="button">
                                <i class="fa-solid fa-fw fa-bell" onclick="deviceToastFunction()"></i>
                            </span>
                            <span id="DeskChatButton" class="deskarea" title="Avaa chat-ikkuna tälle tietokoneelle" role="button">
                                <i class="fa-solid fa-fw fa-message" onclick="deviceChat(event)"></i>
                            </span>
                            <span id="DeskRunButton" cmenu="deskPreConfigScriptContextMenu" class="deskarea" title="Run a script on this computer" 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>Valitse näyttö<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>Näyttönäppäimistö</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>Kirjoita teksti</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>Toiminnot</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>Asetukset</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>Ota kosketuslevytila käyttöön</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>Ota hiiren oikean painikkeen tila käyttöön</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskCustomizeKeys();deskCloseMobileActions()"><i class="fa-solid fa-sliders fa-fw me-2 text-primary"></i>Mukauta pikakuvakkeet</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>Päivitä työpöytä</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>Leikepöytä</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>Lataa leikepöytä</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>Avaa URL etäkoneessa</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>Lukitse etäkone</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>Lähetä ilmoitus</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceChat(event);deskCloseMobileActions()"><i class="fa-solid fa-message fa-fw me-2 text-primary"></i>Chat</a>
                        <a class="d-block px-3 py-2 text-decoration-none" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="runDeviceCmd();deskCloseMobileActions()"><i class="fa-solid fa-play fa-fw me-2 text-primary"></i>Suorita skripti</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="Takaisin" 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>Pääte<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="Koko näyttö. Pidä vaihtonäppäintä selaimen koko näytössä."></i>
                    </div>
                </div>
                <div id="p12warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Uudelleenohjausportti tai KVM-ominaisuus on poistettu käytöstä<span id="p12warninga">, ota se käyttöön napsauttamalla tätä.</span></div>
                </div>
                <div id="p12warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Remote computer is not powered on, click here to issue a power command.
                </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>
                                        Automaattinen yhdistäminen</button>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2span">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2" title="Yhdistä käyttämällä MeshAgent-etätyöpöytää" onclick="connectTerminal(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Yhdistä</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">Vaihda pudotusvalikko</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Järjestelmänvalvoja komentokehote</b></a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0,event)">Järjestelmänvalvojan PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Käyttäjän kuori</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Käyttäjä PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0x10,event)">Kysy Admin Shelliltä</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0x10,event)">Kysy järjestelmänvalvojalta PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0x10,event)">Kysy User Shelliltä</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0x10,event)">Kysy käyttäjältä PowerShell</a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(8,0,event)"><b>Käyttäjän kuori</b></a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Käyttäjä PowerShell</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Juuren kuori</b></a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Käyttäjän kuori</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(100,0,event)">Kirjaudu sisään</a></li>
                                        </ul>
                                    </div>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2sspan">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2s" title="Yhdistä käyttämällä MeshAgent-etätyöpöytää" onclick="connectTerminal(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">SSH-yhteys</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">Vaihda pudotusvalikko</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Vaihtoehtoinen portti</a></li>
                                        </ul>
                                    </div>
                                    <span id="connectbutton2hspan"><button type="button" class="btn btn-success btn-sm me-1" id="connectbutton2h" title="Yhdistä Intel® AMT -laitteiston KVM: llä" onclick="connectTerminal(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">HW Connect</button></span>
                                    <span id="disconnectbutton2span"><button type="button" class="btn btn-danger btn-sm me-1" id="disconnectbutton2" onclick="connectTerminal(event,0)" onkeypress="return false" onkeydown="return false">Katkaise yhteys</button></span>
                                    <span id="termstatus" style="line-height:22px">Yhteys katkaistu</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="Palvelin tallentaa tätä istuntoa" 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="Virranhallintatoimintojen suorittaminen laitteessa" onkeypress="return false" onkeydown="return false" value="Toiminnot" 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="Välilehti" 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="Askelpalautin" 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="Liitä" title="Tekstin liittäminen päätteeseen" onclick="showTermPasteDialog()" style="display:none">
                                </div>
                                <div class="d-flex align-items-center ms-auto">
                                    <span id="TermTimer" title="Istunnon kesto"></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="Vaihda, mitä palautusavain lähettää" 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="Vaihda F1 - F10-näppäinten emulointityyppi" onclick="termToggleFx()">
                                        <input id="id_ttypebutton" type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" value="Laajennettu Ascii" title="Vaihda pääteemulointityyppi" 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="Lähetä" title="Lähetä valittu erikoisavain" 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="Takaisin" 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>Tiedostot<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> Automaattinen yhdistäminen</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connect" title="Yhdistä käyttämällä MeshAgent-etätyöpöytää" onclick="connectFiles(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Yhdistä</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">Vaihda pudotusvalikko</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmconnectfilesaction()">Pyydä Hyväksyntää</a></li>
                                    </ul>
                                </div>
                                <button id="p13Disconnect" class="btn btn-danger btn-sm me-1" onclick="connectFiles(event)" type="button">Katkaise yhteys</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectsspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connects" title="Yhdistä käyttämällä MeshAgent-etätyöpöytää" onclick="connectFiles(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">SFTP-yhteys</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">Vaihda pudotusvalikko</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Vaihtoehtoinen portti</a></li>
                                    </ul>
                                </div>
                                <span id="p13Status">Yhteys katkaistu</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="Virranhallintatoimintojen suorittaminen laitteessa" value="Toiminnot" onclick="deviceActionFunction()">
                                <div id="filesRecordIcon" class="deskareaicon" title="Palvelin tallentaa tätä istuntoa" 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="Ylös" onclick="p13folderup()">
                                <input type="button" disabled="disabled" id="p13SelectAllButton" class="btn btn-secondary me-1 btn-sm" value="Valitse kaikki" onclick="p13selectallfile()">
                                <input type="button" disabled="disabled" id="p13RenameFileButton" class="btn btn-primary me-1 btn-sm" value="Nimeä uudelleen" onclick="p13renamefile()">
                                <input type="button" disabled="disabled" id="p13DeleteFileButton" class="btn btn-primary me-1 btn-sm" value="Poista" onclick="p13deletefile()">
                                <input type="button" disabled="disabled" id="p13ViewFileButton" class="btn btn-primary me-1 btn-sm" value="Muokkaa" onclick="p13viewfile()">
                                <input type="button" disabled="disabled" id="p13NewFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Uusi kansio" onclick="p13createfolder()">
                                <input type="button" disabled="disabled" id="p13NewFileButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Uusi tiedosto" onclick="p13createfile()">
                                <input type="button" disabled="disabled" id="p13UploadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Lähetä" onclick="p13uploadFile()">
                                <input type="button" disabled="disabled" id="p13DownloadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Lataa" onclick="p13downloadButton()">
                                <input type="button" disabled="disabled" id="p13CutButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Leikkaa" onclick="p13copyFile(1)">
                                <input type="button" disabled="disabled" id="p13CopyButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Kopioi" onclick="p13copyFile(0)">
                                <input type="button" disabled="disabled" id="p13PasteButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Liitä" onclick="p13pasteFile()">
                                <input type="button" disabled="disabled" id="p13ZipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Postinumero" onclick="p13zipFiles()">
                                <input type="button" disabled="disabled" id="p13UnzipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Unzip" onclick="p13unzipFile()">
                                <input type="button" disabled="disabled" id="p13RefreshButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Päivitä" onclick="p13folderup(9999)">
                                <input type="button" disabled="disabled" id="p13FindButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="löytö" onclick="p13findfile()">
                                <input type="button" disabled="disabled" id="p13GoToFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="GoTo" onclick="p13gotofolder()">
                                <input type="button" disabled="disabled" id="p13OpenButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Open" onclick="p13openfilefolder()">
                                <div class="btn-group me-1 mobileFileActionDropdown">
                                    <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">Lisää&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()">Uusi kansio</button>
                                        <button type="button" disabled="disabled" id="p13MobileNewFileButton" class="dropdown-item" onclick="p13createfile()">Uusi tiedosto</button>
                                        <button type="button" disabled="disabled" id="p13MobileUploadButton" class="dropdown-item" onclick="p13uploadFile()">Lähetä</button>
                                        <button type="button" disabled="disabled" id="p13MobileDownloadButton" class="dropdown-item" onclick="p13downloadButton()">Lataa</button>
                                        <button type="button" disabled="disabled" id="p13MobileCutButton" class="dropdown-item" onclick="p13copyFile(1)">Leikkaa</button>
                                        <button type="button" disabled="disabled" id="p13MobileCopyButton" class="dropdown-item" onclick="p13copyFile(0)">Kopioi</button>
                                        <button type="button" disabled="disabled" id="p13MobilePasteButton" class="dropdown-item" onclick="p13pasteFile()">Liitä</button>
                                        <button type="button" disabled="disabled" id="p13MobileZipButton" class="dropdown-item" onclick="p13zipFiles()">Postinumero</button>
                                        <button type="button" disabled="disabled" id="p13MobileUnzipButton" class="dropdown-item" onclick="p13unzipFile()">Unzip</button>
                                        <button type="button" disabled="disabled" id="p13MobileRefreshButton" class="dropdown-item" onclick="p13folderup(9999)">Päivitä</button>
                                        <button type="button" disabled="disabled" id="p13MobileFindButton" class="dropdown-item" onclick="p13findfile()">löytö</button>
                                        <button type="button" disabled="disabled" id="p13MobileGoToFolderButton" class="dropdown-item" onclick="p13gotofolder()">GoTo</button>
                                        <button type="button" disabled="disabled" id="p13MobileOpenButton" class="dropdown-item" onclick="p13openfilefolder()">Open</button>
                                    </div>
                                </div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td class="areaHead3">
                            <div class="toright2">
                                <select id="p13sizedropdown" class="form-select-sm me-2" onchange="p13updateFiles()">
                                    <option value="0" selected="selected">Human Readable</option>
                                    <option value="1" title="Bytes">Bytes</option>
                                    <option value="10" title="KB">Kilobytes</option>
                                    <option value="20" title="MB">Megatavua</option>
                                    <option value="30" title="GB">Gigabyte</option>
                                </select>
                                <select id="p13sortdropdown" class="form-select-sm me-2" onchange="p13updateFiles()">
                                    <option value="1" selected="selected">Lajittele nimen mukaan</option>
                                    <option value="2">Lajittele koon mukaan</option>
                                    <option value="3">Lajittele päivämäärän mukaan</option>
                                    <option value="4">Laskeva nimen mukaan</option>
                                    <option value="5">Laskeva koon mukaan</option>
                                    <option value="6">Laskeva päivämäärän mukaan</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="Takaisin" 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="Koko näyttö. Pidä vaihtonäppäintä selaimen koko näytössä."></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="Takaisin" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Konsoli<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="Tietoja tämän agentin nykyisestä ytimestä"></div>
                                <input type="button" id="p15uploadCore" class="btn btn-primary me-1 btn-sm" value="Agentin toiminta" onclick="p15uploadCore(event)" title="Vaihda agentti Java Script -koodimoduuli">
                                <i onclick="p15downloadConsoleText()" title="Lataa konsolin teksti" 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">Agentti</option>
                                            <option id="p15outputselect3" value="3">Työntää</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="Tyhjennä" 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="Takaisin" 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>Tapahtumat<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">Suodata</label>
                                <select id="p16filterevents" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="">All Logs</option>
                                    <option notransval="1" value="agentlog">Agent Logs</option>
                                    <option notransval="1" value="relaylog">Relay Logs</option>
                                    <option notransval="1" value="manual">Manual Logs</option>
                                    <option notransval="1" value="runcommands">Run Command Logs</option>
                                    <option notransval="1" value="batchupload">Batch Upload Logs</option>
                                    <option notransval="1" value="changenode">Change Node Logs</option>
                                    <option notransval="1" value="removenode">Remove Node Logs</option>
                                </select>
                                <label class="me-1" for="p16limitdropdown">Näytä</label>
                                <select id="p16limitdropdown" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="60">Viimeiset 60</option>
                                    <option notransval="1" value="120">Viimeiset 120</option>
                                    <option notransval="1" value="250">Viimeiset 250</option>
                                    <option notransval="1" value="500">Viimeiset 500</option>
                                    <option notransval="1" value="1000">Viimeiset 1000</option>
                                </select>
                                <i onclick="p3showDownloadEventsDialog(1)" class="fa-solid fa-download" title="Lataa tapahtumat" 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="Takaisin" 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>Tarkemmat tiedot<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="Päivitä yksityiskohtaiset tiedot."></i>
                        <i class="fa-solid fa-chart-line fa-xl fa-border" role="button" onclick="deskToggleCpuGraph(event)" title="Näytä laitteen suorittimen ja muistin käyttö."></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>Live-graafi</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="Takaisin" 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>Ohjelmisto<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="Päivitä" onclick="loadInstalledApps()">
                        <input type="text" id="p18SearchInput" class="form-control-sm" placeholder="Etsi ohjelmistoa..." style="width:250px" onkeyup="filterInstalledApps()">
                        <label style="margin-left:15px">
                            <input type="checkbox" id="p18ShowStore" onchange="toggleStoreApps()"> Näytä kauppasovellukset
                        </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>Napsauta Päivitä ladataksesi asennetun ohjelmiston...</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="Takaisin" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Liitännäiset<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="Takaisin" onkeypress="if (event.key == 'Enter') goBack()"></i>
                        </div>
                        <div class="fs-4 fw-bold">Yleistä<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="Takaisin" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Yhteenveto - <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">Virta tilat</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">Agenttityypit</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">Liitettävyys</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">Turvallisuus</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="Takaisin" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Yleistä<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="Takaisin" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Tapahtumat<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">Suodata</label>
                                <select id="p31filterevents" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="">All Logs</option>
                                    <option notransval="1" value="agentlog">Agent Logs</option>
                                    <option notransval="1" value="relaylog">Relay Logs</option>
                                    <option notransval="1" value="manual">Manual Logs</option>
                                    <option notransval="1" value="runcommands">Run Command Logs</option>
                                    <option notransval="1" value="batchupload">Batch Upload Logs</option>
                                    <option notransval="1" value="changenode">Change Node Logs</option>
                                    <option notransval="1" value="removenode">Remove Node Logs</option>
                                </select>
                                <label for="p31limitdropdown" class="me-1">Näytä</label>
                                <select id="p31limitdropdown" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="60">Viimeiset 60</option>
                                    <option notransval="1" value="120">Viimeiset 120</option>
                                    <option notransval="1" value="250">Viimeiset 250</option>
                                    <option notransval="1" value="500">Viimeiset 500</option>
                                    <option notransval="1" value="1000">Viimeiset 1000</option>
                                    <option notransval="1" value="">Ei rajoitusta</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(3)"><i class="fa-solid fa-download" role="button" title="Lataa tapahtumat"></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">Palvelimeni tilastot</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Päivitä" 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">Loki-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">Yhteyksiä</option>
                            <option value="1">Muisti</option>
                            <option value="5">Prosessori</option>
                            <option value="3">Saapuva liikenne</option>
                            <option value="4">Lähtevä liikenne</option>
                        </select>
                        <select id="p40time" class="form-select-sm me-1" onchange="updateServerTimelineHours()">
                            <option value="3">Viimeiset 3 tuntia</option>
                            <option value="8">Viimeiset 8 tuntia</option>
                            <option value="24">Viime päivä</option>
                            <option value="168">Viime viikko</option>
                            <option value="720">Viimeiset 30 päivää</option>
                        </select>
                        <i class="fa-solid fa-download" title="Lataa data pisteet (.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">Oma palvelimen jäljitys</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Jäljitä" type="button" class="btn btn-primary btn-sm me-1" onclick="setServerTracing()">
                        <span id="p41traceStatus">Ei mitään</span>
                    </div>
                    <div class="toright2 d-flex align-items-center ms-auto">
                        <label for="p41limitdropdown" class="me-1">Näytä</label>
                        <select id="p41limitdropdown" class="form-select-sm me-1" onchange="displayServerTrace()">
                            <option value="100">Viimeiset 100</option>
                            <option value="250">Viimeiset 250</option>
                            <option value="500">Viimeiset 500</option>
                            <option value="1000">Viimeiset 1000</option>
                        </select>
                        <input value="Tyhjennä" type="button" class="btn btn-primary btn-sm me-1" onclick="clearServerTracing()">
                        <i class="fa-solid fa-download" title="Lataa jäljitys (.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">Oma palvelin -laajennukset</div>
                </div>
                <div class="areaHead">
                    <div class="toright2">
                    </div>
                    <div>
                        <input value="Lataa laajennus" type="button" onclick="return pluginHandler.addPluginDlg();">
                    </div>
                </div>
                <div id="pluginRestartNotice" class="areaHead" style="background-color:gold;display:none">
                    <div class="toright2">
                        <input value="Päivitä agenttien ytimet" type="button" onclick="distributeCore();return false">
                    </div>
                    <div style="padding:2px">
                        <div style="padding:2px"><b>Ilmoitus:</b> Laajennuksia on muutettu, mikä saattaa edellyttää agentin ytimen päivitystä.</div>
                    </div>
                </div>
                <table id="p42tbl">
                    <tbody><tr class="DevSt">
                        <th style="width:26px"></th>
                        <th style="width:10px"></th>
                        <th class="chName">Nimi</th>
                        <th class="chDescription">Kuvaus</th>
                        <th class="chSite" style="text-align:center">Linkki</th>
                        <th class="chVersion" style="text-align:center">Versio</th>
                        <th class="chUpgradeAvail" style="text-align:center">Viimeisin</th>
                        <th class="chStatus" style="text-align:center">Tila</th>
                        <th class="chAction" style="text-align:center">Toiminta</th>
                        <th style="width:10px"></th>
                    </tr>
                </tbody></table>
                <div id="pluginNoneNotice" style="width:100%;text-align:center;padding-top:10px;display:none"><i>Ei palvelimen laajennuksia.</i></div>
            </div>
            <div id="p43" style="display:none">
                <div id="p43BackButton">
                    <div class="backButton" tabindex="0" onmouseup="go(42)" title="Takaisin" onkeypress="if (event.key == 'Enter') go(42)">
                        <div class="backButtonEx"></div>
                    </div>
                </div>
                <h1>Oma palvelinlaajennukset - <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">Omat käyttäjäryhmät</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="Valitse kaikki">
                                <input type="button" id="UsersGroupsGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Ryhmätoiminta" onclick="p50usersGroupActionFunction()">
                                <input id="NewUserGroupButton" type="button" class="btn btn-success btn-sm me-1" onclick="showCreateUserGroupDialog(1)" value="Uusi ryhmä...">
                                <input id="DuplicateUserGroupButton" type="button" class="btn btn-primary button-outline-success btn-sm me-1" style="display:none" onclick="showCreateUserGroupDialog(2)" value="Monista Ryhmä...">
                            </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="Takaisin" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Käyttäjäryhmä - <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">Omat käyttäjän tallenteet</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="Päivitä">
                            </div>
                            <div class="d-flex align-items-center ms-auto">
                                <input type="button" class="btn btn-primary btn-sm" onclick="openRecodringPlayer()" value="Avaa pelaaja ...">
                            </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">Omat raportit</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="Lataa raportti"></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> Luo raportti...</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()">Vahvista sähköpostiosoite</a>
                        &nbsp;<a id="termsLinkFooter" href="terms">Ehdot ja yksityisyys</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>Tiedoston valinta</div>
                                <select id="d3uploadMode" onchange="d3modechange()">
                                    <option value="1">Paikallinen tiedosto ladataan</option>
                                    <option value="2">Palvelintiedoston valinta</option>
                                </select>
                            </div>
                            <div id="d3localmode" style="display:none">
                                <div>Lataa tiedosto</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="Ylös">&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="Kääri päälle" onclick="d4ToggleWrap()">
                            <input id="d4SizeButton" class="btn btn-primary me-1 mb-1" type="button" value="Pieni" onclick="d4ToggleSize()">
                            <input id="d4EncodingButton" class="btn btn-primary me-1 mb-1" type="button" value="Raaka" 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')">Agentti</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">Laatu</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">Skaalaus</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7framelimiter" class="form-select">
                                        <option selected="selected" value="50">Nopea</option>
                                        <option value="100">Keskikokoinen</option>
                                        <option value="400">Hidas</option>
                                        <option value="1000">Hyvin hidas</option>
                                    </select>
                                    <label for="d7framelimiter">Ruudunpäivitysnopeus</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">Koodaus</label>
                                </div>
                                <fieldset id="d7desktopOtherSettings">
                                    <legend>Muut asetukset</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">Vaihda hiiren painikkeet</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskrmw" class="form-check-label">Käänteinen hiiren pyörä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskRemoteKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskRemoteKeyMap" class="form-check-label">Käytä kaukonäppäimistökarttaa</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">Automaattinen leikepöytä</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">Lukitse Katkaise yhteys</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, Nopein</option>
                                        <option value="2">RLE16, Suositeltu</option>
                                        <option value="3">RAW8, Hidas</option>
                                        <option value="4">RAW16, Hyvin hidas</option>
                                    </select>
                                    <label for="d7desktopmode">Kuvan koodaus</label>
                                </div>
                                <fieldset>
                                    <legend>Muut asetukset</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">Näytä tarkennustyökalu</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7showcursor" class="form-check-input me-2" type="checkbox">
                                            <label for="d7showcursor" class="form-check-label">Näytä paikallinen hiiren kohdistin</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7localKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7localKeyMap" class="form-check-label">Paikallinen näppäimistökartta</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7kvmrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7kvmrmw" class="form-check-label">Käänteinen hiiren pyörä</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">Kankaan koko</option>
                                        <option notransval="1" value="browser">Selaimen koko</option>
                                        <option notransval="1" value="screen">Näytön koko</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">Näytön koko</label>
                                </div>
                                <fieldset>
                                    <legend>Vaihtoehdot</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">Poista taustakuva käytöstä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp2" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp2" class="form-check-label">Poista koko ikkunan vetäminen käytöstä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp3" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp3" class="form-check-label">Poista valikon animaatiot käytöstä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp4" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp4" class="form-check-label">Poista Theming käytöstä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp6" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp6" class="form-check-label">Poista kohdistimen varjo käytöstä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp7" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp7" class="form-check-label">Poista kohdistimen asetukset käytöstä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp8" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp8" class="form-check-label">Ota fontin tasoitus käyttöön</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp9" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp9" class="form-check-label">Ota työpöydän kokoonpano käyttöön</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpclip" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpclip" class="form-check-label">Automaattinen leikepöytä</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpsmb" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpsmb" class="form-check-label">Vaihda hiiren painikkeet</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdprmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdprmw" class="form-check-label">Käänteinen hiiren pyörä</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">Peruuta</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">Poista</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 = ['', "Käynnissä", "Nukkua", "Nukkua", "Nukkua", "Horroksessa", "Sammuta", "Läsnä"];
        var StatusStrs = ["Yhteys katkaistu", "Yhdistetään...", "Asennus...", "Yhdistetty", "Intel&reg; AMT Yhdistetty"];
        var agentsStr = ["Tuntematon", "Windows 32-bittinen konsoli", "Windows 64-bittinen konsoli", "Windows 32bit-palvelu", "Windows 64bit-palvelu", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86-32-bittinen", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86-32bit", "macOS x86-64-bittinen", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Windows MinCore -konsoli", "Windows MinCore -palvelu", "SolmuJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bit", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Apple Silicon", "FreeBSD x86-64", "Tuntematon", "Linux ARM 64 bit", "Alpine Linux x86 64 Bit (MUSL)", "Assistant (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Tuntematon", "Tuntematon", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Windows ARM 64bit console", "Windows ARM 64bit service", "ARMVIRT32 (OpenWRT)", "RISC-V 64bit"];
        var agentsStrNoAgent = ['', '', '', '', "Windows", '', "Linux", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', "Omena"];
        var domainStates = ["Ei mitään", "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]|Tyyppi");

            // 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>' + "Selaimen oletus" + '</label><br>';
            x += '<input type=radio id=night2 name=nightmoderadio value=2 ' + ((cNightMode == 2) ? 'checked' : '') + '><label for=night2>' + "Valotila" + '</label><br>';
            x += '<input type=radio id=night1 name=nightmoderadio value=1 ' + ((cNightMode == 1) ? 'checked' : '') + '><label for=night1>' + "Tumma tila" + '</label><br>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Yötila", 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', "Käyttöliittymä", 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', "Todennusta ei voida suorittaa"); return; }
                if (errorCode == 'invalidorigin') { QH('p0span', "Invalid origin in HTTP request"); return; }
                if (prevState == 2) { if (autoReconnect) { setTimeout(serverPoll, 5000); } } else { QH('p0span', "Verkkopistoketta ei voida yhdistää"); }
                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: "Lisää kahden tekijän varmuuskoodit. Jos nykyinen tekijä menetetään, tätä tiliä ei voida palauttaa.", title: "Kaksivaiheinen todennus", 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', " - Nollaa seuraavan kirjautumisen yhteydessä."); }
                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', " - Nollaa seuraavan kirjautumisen yhteydessä."); }
                    else if (seconds < 3600) { var secs = Math.floor(seconds / 60); QH('p2nextPasswordUpdateTime', format((secs == 1) ? " - Nollaa 1 minuutissa." : " - Nollaa {0} minuutissa.", secs)); }
                    else if (seconds < 86400) { var hours = Math.floor(seconds / 3600); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Nollaa 1 tunnissa." : " - Nollaa {0} tunnissa.", hours)); }
                    else { var days = Math.floor(seconds / 86400); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Nollaa 1 päivässä." : " - Nollaa {0} päivässä.", 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 sekunti sisäänkirjautumiseen" : "{0} sekuntia sisäänkirjautumiseen asti")
                        : (ds == 1 ? "1 sekuntti yhteyden katkaisuun" : "{0} sekuntia yhteyden katkaisuun");
                } else {
                    ds = Math.round(ds / 60);
                    if (ds <= 5) {
                        theText = isLogout
                            ? (ds == 1 ? "1 minuutti sisäänkirjautuminen" : "{0} minuutti sisäänkirjautumiseen")
                            : (ds == 1 ? "1 minuutti yhteyden katkaisuun" : "{0} minuuttia, yhteyden sulkemiseen");
                    }
                }
                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', "Ei mitään");
                        }
                    }
                    break;
                }
                case 'serverstats': {
                    updateGeneralServerStats(message);
                    break;
                }
                case 'serverwarnings': {
                    if ((message.warnings != null) && (message.warnings.length > 0)) {
                        var ServerWarnings = {
                            1: "",
                            2: "Puuttuvat WebDAV-parametrit.",
                            3: "Tuntematon määritysvaihtoehto \"{0}\".",
                            4: "WebSocket-pakkaus on poistettu käytöstä, tämä ominaisuus on rikki NodeJS v11.11 - v12.15 ja v13.2",
                            5: "Intel AMT TLS -juurivarmennetta ei voi ladata oletustoimialueelle.",
                            6: "Ei voida ladata Intel AMT TLS -juurivarmennetta verkkotunnukselle {0}.",
                            7: "CIRA:n paikalliset FQDN:t ohitetaan, kun palvelin on vain LAN- tai WAN-tilassa.",
                            8: "CIRA:n paikallista FQDN:ää voi olla enintään 4. Arvon huomioiminen.",
                            9: "Agentin hajautustarkistus ohitetaan, tämä ei ole turvallista.",
                            10: "Let's Encrypt -sähköpostiosoite puuttuu.",
                            11: "Virheelliset Let's Encrypt -isäntänimet.",
                            12: "Virheelliset Let's Encrypt -nimet, ei voi sisältää *-merkkiä.",
                            13: "Let's Encrypt -moduulia ei voi asentaa.",
                            14: "Virheelliset Let's Encrypt -nimet, ei voi ratkaista: {0}",
                            15: "Virheellinen Let's Encrypt -sähköpostiosoite, ei voida ratkaista: {0}",
                            16: "CloudFlaren luotettujen välityspalvelinten IPv6-osoiteluetteloa ei voi ladata.",
                            17: "SendGrid-palvelimen käyttö on rajoitettua LAN-tilassa.",
                            18: "SMTP-palvelimen käyttö on rajoitettua LAN-tilassa.",
                            19: "SMS-yhdyskäytävän käyttö on rajoitettua LAN-tilassa.",
                            20: "Virheellinen \"LoginCookieEncryptionKey\" tiedostossa config.json.",
                            21: "Varmuuskopiointipolkua ei voi määrittää meshcentral-data-kansiossa, varmuuskopiointiasetuksia ei huomioida.",
                            22: "Agentin {0} allekirjoittaminen epäonnistui: {1}",
                            23: "Unable to load agent icon file: {0}.",
                            24: "Unable to load agent logo file: {0}.",
                            25: "This NodeJS version does not support OpenID.",
                            26: "This NodeJS version does not support 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>' + "VAROITUS: " + 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>' + "VAROITUS: " + 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>' + "VAROITUS: " + format("Varmenne vanhenee {0} päivän kuluttua", days) + '</b></div>');
                            QV('serverWarningsDiv', true);
                            addNotification({ text: format("Varmenne vanhenee {0} päivän kuluttua", 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>' + "VIRHE:" + JSON.parse(message.value).error + '</span>');
                            QH('p18html', '<div style="text-align:center;padding:50px;color:gray"><i>' + "Napsauta Päivitä ladataksesi asennetun ohjelmiston..." + '</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>' + "Viimeksi nähty:" + '<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>' + "Onnistui" + '</span>' : '<span style=color:red>' + "Epäonnistui" + '</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>' + "Prosessin nimi" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.processName + '</b></div></div>'; }
                                        if (message.value.machineName) { x += addHtmlValue5("Koneen nimi", message.value.machineName); }
                                        if (message.value.cmd) { x += '<div style=padding:4px><div style=padding-bottom:4px>' + "Komentorivi" + '</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>' + "Ikkunan otsikko" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.mainWindowTitle + '</b></div></div>'; }
                                        if (message.value.processUser) { x += addHtmlValue5("Käyttäjä", message.value.processUser); }
                                        if (message.value.processDomain) { x += addHtmlValue5("Verkkotunnus", message.value.processDomain); }
                                        if (message.value.startTime) { x += addHtmlValue5("Käynnistys aika", message.value.startTime); }
                                        x += '<hr />';
                                        if (message.value.PriorityBoostEnabled) { x += addHtmlValue5("Priority Boost", message.value.PriorityBoostEnabled ? "Käytössä" : "Poistettu käytöstä"); }
                                        if (message.value.sessionId) { x += addHtmlValue5("Istunnon tunniste", message.value.sessionId); }
                                        if (message.value.handleCount) { x += addHtmlValue5("Käsittele laskua", message.value.handleCount); }
                                        if (message.value.privilegedProcessorTime) { x += addHtmlValue5("Privileged -prosessorin aika", format("{0} sekuntia", message.value.privilegedProcessorTime)); }
                                        if (message.value.totalProcessorTime) { x += addHtmlValue5("Prosessorin kokonaisaika", format("{0} sekuntia", message.value.totalProcessorTime)); }
                                        if (message.value.userProcessorTime) { x += addHtmlValue5("Käyttäjän prosessorin aika", format("{0} sekuntia", message.value.userProcessorTime)); }
                                        if (message.value.nonpagedSystemMemorySize) { x += addHtmlValue5("Sivuton muisti", getNiceSize2(message.value.nonpagedSystemMemorySize)); }
                                        if (message.value.pagedMemorySize) { x += addHtmlValue5("Sivumuisti", getNiceSize2(message.value.pagedMemorySize)); }
                                        if (message.value.privateMemorySize) { x += addHtmlValue5("Yksityinen muisti", getNiceSize2(message.value.privateMemorySize)); }
                                        if (message.value.virtualMemorySize) { x += addHtmlValue5("Virtuaalimuisti", getNiceSize2(message.value.virtualMemorySize)); }
                                        if (message.value.workingSet) { x += addHtmlValue5("Toimiva sarja", getNiceSize2(message.value.workingSet)); }
                                        if (message.value.peakWorkingSet) { x += addHtmlValue5("Peak Working Set", getNiceSize2(message.value.peakWorkingSet)); }
                                        if (message.value.peakPagedMemorySize) { x += addHtmlValue5("Peak Paged -muisti", getNiceSize2(message.value.peakPagedMemorySize)); }
                                        if (message.value.peakVirtualMemorySize) { x += addHtmlValue5("Huippu virtuaalimuisti", getNiceSize2(message.value.peakVirtualMemorySize)); }
                                    } else {
                                        x += "Ei tietoja";
                                    }
                                    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("Ensimmäinen agenttiyhteys", printDateTime(new Date(currentNode.firstconnect))); }
                        if (currentNode.lastconnect) { x += addHtmlValue2("Agentin edellinen yhteys", printDateTime(new Date(currentNode.lastconnect))); }
                        if (currentNode.lastaddr) {
                            var splitip = currentNode.lastaddr.split(':');
                            if (splitip.length > 2) {
                                // IPv6
                                x += addHtmlValue2("Agentin edellinen osoite", currentNode.lastaddr + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(currentNode.lastaddr) + '")></i>');
                            } else {
                                // IPv4
                                if (isPrivateIP(currentNode.lastaddr)) {
                                    x += addHtmlValue2("Agentin edellinen osoite", splitip[0] + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                } else {
                                    x += addHtmlValue2("Agentin edellinen osoite", '<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="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                }
                            }
                        }
                        if (message.updateTime != null) { x += addHtmlValue2("Viimeisin liittymien päivitys", 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("Nimi", '<b>' + EscapeHtml(net.name) + '</b>'); }
                                if (net.desc) { x += addHtmlValue2("Kuvaus", EscapeHtml(net.desc).replace('(R)', '&reg;').replace('(r)', '&reg;')); }
                                if (net.dnssuffix) { x += addHtmlValue2("DNS-liite", EscapeHtml(net.dnssuffix) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi nimi leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.dnssuffix) + '")></i>'); }
                                if (net.mac) { x += addHtmlValue2("MAC osoite", '<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="' + "Kopioi MAC osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.mac.toLowerCase()) + '")></i>'); }
                                if (net.v4addr) { x += addHtmlValue2("IPv4 osoite", EscapeHtml(net.v4addr) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4addr) + '")></i>'); }
                                if (net.v4mask) { x += addHtmlValue2("IPv4 peite", EscapeHtml(net.v4mask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4mask) + '")></i>'); }
                                if (net.v4gateway) { x += addHtmlValue2("IPv4 yhdyskäytävä", EscapeHtml(net.v4gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4gateway) + '")></i>'); }
                                if (net.gatewaymac) { x += addHtmlValue2("Yhdyskäytävän MAC osoite", '<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="' + "Kopioi MAC osoite leikepöydälle" + '" 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("Nimi", '<b>' + EscapeHtml(i) + '</b>');
                                if (typeof net[0].mac == 'string') { x += addHtmlValue2("MAC osoite", '<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="' + "Kopioi MAC osoite leikepöydälle" + '" 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 osoite", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("IPv6 peite", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("IPv6 yhdyskäytävä", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    } else if (netif.family == 'IPv4') {
                                        if (netif.address) { x += addHtmlValue2("IPv4 osoite", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("IPv4 peite", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("IPv4 yhdyskäytävä", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Kopioi osoite leikepöydälle" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    }
                                }
                            }
                        } else {
                            x += '<hr />'
                            x += "Tälle laitteelle ei ole käytettävissä verkkoliittymien tietoja.";
                        }
                        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 = "Tuntematon"; }
                        if (!message.tags.stable) { message.tags.stable = "Tuntematon"; }
                        if (!message.tags.latest) { message.tags.latest = "Tuntematon"; }
                        x += addHtmlValue2("Nykyinen versio", '<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 == "Tuntematon") || (message.tags.current == message.tags.stable) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Vakaa versio" + '</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 == "Tuntematon") || (message.tags.current == message.tags.latest) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Viimeisin versio" + '</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 Versio", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        } else {
                            x += '<hr />' + "Valitse ja napsauta OK aloittaaksesi palvelimen itsepäivityksen.";
                            xxdialogTag = message.tags;
                            setModalContent('xxAddAgent', "MeshCentral Versio", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                                server_showVersionDlgEx(2, message.tags);
                            });
                            server_showVersionDlgUpdate();
                        }
                    }
                    break;
                }
                case 'servererrors': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'MeshCentralServerErrors')) {
                        if (message.data == null) {
                            setModalContent('xxAddAgent', "Server Errors", "Palvelimella ei ole virhelokia.");
                            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="' + "Lataa virhe loki" + '" onclick=d2CopyServerErrorsToClip()></i></div>';
                            x += '<div><label><input id=d2clearErrorsCheck type=checkbox class="form-check-input me-2" onclick=server_showErrorsDlgUpdate() /> ' + "Valitse ja napsauta OK tyhjentääksesi virhelokin." + '</label></div>';
                            setModalContent('xxAddAgent', "MeshCentral-palvelinvirheet", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', server_showErrorsDlgEx);
                            server_showErrorsDlgUpdate();
                        }
                    }
                    break;
                }
                case 'serverconfig': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'MeshCentralServerConfig')) {
                        if (message.data == null) {
                            setModalContent('xxAddAgent', "Server Configuration", 'Server has no config file.', 'extra-large');
                        } else {
                            setModalContent('xxAddAgent', "Server Configuration", 4, 'extra-large');
                            QV('d4EncodingButton', false);
                            QV('d4LineBreakButton', false);
                            Q('d4editorarea').value = message.data;
                            Q('d4editorarea').setAttribute('readonly', 'readonly');
                        }
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    }
                    break;
                }
                case 'serverconsole': {
                    p15consoleReceive('serverconsole', message.value);
                    break;
                }
                case 'events': {
                    if ((message.nodeid != null) && (currentNode != null) && (message.nodeid == currentNode._id)) {
                        currentDeviceEvents = message.events;
                        mainUpdate(1024);
                    } else if ((message.userid != null) && (currentUser != null) && (message.userid == currentUser._id)) {
                        currentUserEvents = message.events;
                        mainUpdate(2048);
                    } else {
                        events = message.events;
                        mainUpdate(32);
                    }
                    break;
                }
                case 'recordings': {
                    p52recordings = message.events;
                    if (message.error != null) { p52recordings = message.error; }
                    updateRecordings();
                    break;
                }
                case 'getcookie': {
                    if (message.tag == 'MCRouter') {
                        var servername = serverinfo.name;
                        if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
                        var domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1);
                        var portStr = (serverinfo.port == 443) ? '' : (':' + serverinfo.port);
                        var url = 'mcrouter://' + servername + portStr + domainUrl + 'control.ashx?c=' + authCookie + '&t=' + serverinfo.tlshash + '&l={{{lang}}}' + (urlargs.key ? ('&key=' + urlargs.key) : '');
                        if (message.nodeid != null) { url += ('&nodeid=' + message.nodeid); }
                        if (message.tcpport != null) { url += ('&protocol=1&remoteport=' + message.tcpport); }
                        if (message.localRelay) { url += '&local=1'; }
                        if (message.localport) { url += '&localport=' + message.localport; }
                        if (message.name != null) { url += ('&name=' + encodeURIComponentEx(message.name)); }
                        if (message.ip != null) { url += ('&remoteip=' + message.ip); }
                        url += ('&appid=' + message.protocol + '&autoexit=1'); // Protocol: 0 = Custom, 1 = HTTP, 2 = HTTPS, 3 = RDP, 4 = PuTTY, 5 = WinSCP, 6 = MCRDesktop, 7 = MCRFiles
                        //console.log(url);
                        downloadFile(url, '');
                    } else if (message.tag == 'novnc') {
                        var vncurl = window.location.origin + domainUrl + 'novnc/vnc.html?ws=wss%3A%2F%2F' + window.location.host + encodeURIComponentEx(domainUrl) + (message.localRelay ? 'local' : 'mesh') + 'relay.ashx%3Fauth%3D' + message.cookie + '&show_dot=1' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '&l={{{lang}}}';
                        var node = getNodeFromId(message.nodeid);
                        if (node != null) { vncurl += '&name=' + encodeURIComponentEx(node.name); }
                        safeNewWindow(vncurl, 'mcnovnc/' + message.nodeid);
                    } else if (message.tag == 'mstsc') {
                        var rdpurl = window.location.origin + domainUrl + 'mstsc.html?ws=' + message.cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
                        var node = getNodeFromId(message.nodeid);
                        if (node != null) { rdpurl += '&name=' + encodeURIComponentEx(node.name); }
                        if (message.localRelay) { rdpurl += '&local=1'; }
                        safeNewWindow(rdpurl, 'mcmstsc/' + message.nodeid);
                    } else if (message.tag == 'ssh') {
                        var sshurl = window.location.origin + domainUrl + 'ssh.html?ws=' + message.cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
                        var node = getNodeFromId(message.nodeid);
                        if (node != null) { sshurl += '&name=' + encodeURIComponentEx(node.name); }
                        if (message.localRelay) { sshurl += '&local=1'; }
                        safeNewWindow(sshurl, 'mcssh/' + message.nodeid);
                    }
                    break;
                }
                case 'getNotes': {
                    var n = Q('d2devNotes');
                    if (n && (message.id == decodeURIComponent(n.attributes['noteid'].value))) {
                        if (message.notes) { QH('d2devNotes', decodeURIComponent(message.notes)); } else { QH('d2devNotes', ''); }
                        var ro = (n.attributes['ro'].value == 'true');
                        if (ro == false) { // If we have permissions, set read/write on this note.
                            n.removeAttribute('readonly');
                            QE('idx_dlgOkButton', true);
                            QV('idx_dlgOkButton', true);
                            focusTextBox('d2devNotes');
                        }
                    } else {
                        Q('notesPanelArea').innerHTML = (message.notes && marked && DOMPurify) ? DOMPurify.sanitize(marked.parse(decodeURIComponent(message.notes), { breaks: true }), { USE_PROFILES: { html: true } }) : '';
                        if ((showNotesPanel === 'true') && message.notes) { QV('notesPanel', true); } else { QV('notesPanel', false); }
                    }
                    break;
                }
                case 'otpauth-request': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-request')) {
                        if (message.err != null) {
                            var otpauthErrors = ['', "2FA on lukittu", "Varakoodit on lukittu", "Kirjautumistunnus käytössä", "OTP 2FA ei sallittu", "Tili on lukittu", "OTPLIB:n lataaminen epäonnistui"];
                            if ((message.err > 0) && (message.err < otpauthErrors.length)) { QH('d2optinfo', otpauthErrors[message.err]); } else { QH('d2optinfo', format("Virhe #{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("Asenna" + ' <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" rel="noreferrer noopener" target=_blank>' + "Google Authenticator" + '</a> ' + "tai yhteensopiva sovellus ja skannaa viivakoodi, käytä <a href=\"{0}\" rel=\"noreferrer noopener\" target=_blank>tätä linkkiä</a> tai kirjoita salainen avain. Anna sitten alla oleva nykyinen 6-numeroinen tunnus aktivoidaksesi kaksivaiheisen kirjautumisen.", message.url) + '<br /><br />' + 'Secret <img src=images/link4.png height=10 width=10 title="' + "Copy Secret to clipboard" + '" style=cursor:pointer onclick=d2CopySecretToClip()>' + '<br /><tt id=d2optsecret secret="' + message.secret + '" style=font-size:12px>' + secret + '</tt><br /><br /></td><td style=width:1px;vertical-align:top><a href="' + message.url + '" rel="noreferrer noopener" target=_blank><div id="qrcode"></div></a></td><tr><td colspan=2 style="text-align:center;border-top:1px solid black"><br />' + "Kirjoita kaksivaiheisen kirjautumisen tunnus tähän:" + ' <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', "Todennussovellus", message.success ? ('<b style=color:green>' + "Todennussovelluksen aktivointi onnistui." + '</b> ' + "Tarvitset nyt kelvollisen tunnuksen kirjautumiseen uudelleen.") : ('<b style=color:red>' + "Kaksivaiheisen kirjautumisen aktivointi epäonnistui." + '</b> ' + "Poista salainen avain sovelluksesta ja yritä uudelleen. Sinulla on vain muutama minuutti oikean koodin syöttämiseen."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-clear': {
                    if (xxdialogMode) return;
                    setModalContent('xxAddAgent', "Todennussovellus", message.success ? ('<b>' + "Todennussovellus poistettu." + '</b> ' + "Voit aktivoida tämän ominaisuuden uudelleen milloin tahansa.") : ('<b style=color:red>' + "Kaksivaiheisen sisäänkirjautumisen aktivoinnin poisto epäonnistui." + '</b> ' + "Yritä uudelleen."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-getpasswords': {
                    if (xxdialogMode == 2 && (xxdialogTag != 'otpauth-manage')) return;
                    var x = "Yhden kerran vahvistus koodeja voidaan käyttää toissijaisena todennuksena. Luo sarja, tulosta ne ja säilytä niitä turvallisessa paikassa.";
                    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>' + "Ei aktiivisia tunnuksia";
                    }
                    x += '</table></div></div><br />';
                    x += '<div class="btn-group">';
                    x += '<button type=button class="btn btn-warning" onclick="account_manageOtp(1);">' + "Luo uusia tunnuksia" + '</input>';
                    if (message.passwords != null) {
                        x += '<button type=button class="btn btn-danger" onclick="account_manageOtp(2);">' + "Tyhjennä valtuudet" + '</button></div>';
                        x += '&nbsp;<i class="fa-regular fa-clipboard" title="' +  "Kopioi kelvolliset koodit leikepöydälle" + '" style="cursor:pointer" onclick=copyTextToClip2("' + encodeURIComponentEx(clipb) + '")></i>';
                    } else {
                        x += '</div>'
                    }
                    xxdialogTag = 'otpauth-manage';
                    setModalContent('xxAddAgent', "Hallinnoi varmuuskoodeja", 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">' + "Laiteavaimia " + '</a> ' + "käytetään toissijaisena kirjautumistunnistuksena.";
                    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 + ')>' + "Poista" + '</button>' + end;
                        }
                    } else {
                        x += start + '<tr style=text-align:center><td>' + "Ei avaimia määritetty" + 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);">' + "Lisää avain" + '</input>'; }
                        if ((features & 0x00004000) != 0) { x += '<button class="btn btn-primary" id=d2addkey2 type=button onclick="account_addhkey(2);">' + "Lisää YubiKey&reg; OTP" +'</input>'; }
                    } else {
                        x += "Avainten enimmäismäärä saavutettu.";
                    }
                    x += '</div><br />';
                    xxdialogTag = 'otpauth-hardware-manage';
                    setModalContent('xxAddAgent', "Hallinnoi suojausavaimia", 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', "Lisää suojausavain", '<br />' + "Virhe, avainta ei voitu lisätä." + '<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', "Lisää suojausavain", '<br />' + "Virhe, avainta ei voitu lisätä." + '<br /><br />');
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');

                    }
                    break;
                }
                case 'webauthn-startregister': {
                    if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
                    var x = "Paina avain nappia nyt." + '<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', "Lisää suojausavain", 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("VIRHE:" + error);
                        setModalContent('xxAddAgent', "Lisää suojausavain", "VIRHE:" + 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>' + "Tarkista puhelimesi ja kirjoita vahvistuskoodi.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Vahvistuskoodi:" + ' <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', "Puhelin ilmoitukset", 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>' + "Check your messaging application and enter the verification code.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Vahvistuskoodi:" + ' <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', "Messaging Notifications", 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("Apua pyydettiin käyttäjältä {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 = "Agentti yhdistetty";
                                    if ((node.agent != null) && (node.agent.root === false)) { agentPrivilages = "Agentti, jolla on rajoitetut oikeudet"; }
                                    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 kytketty"; addNotification(abc); }
                                    if (((node.conn & 4) == 0) && ((message.event.conn & 4) != 0)) { abc.text = "Intel AMT kytketty"; addNotification(abc); }
                                    if (((node.conn & 16) == 0) && ((message.event.conn & 16) != 0)) { abc.text = "MQTT yhdistetty"; 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 = "Agentti irrotettu"; addNotification(abc); }
                                    if (((node.conn & 2) != 0) && ((message.event.conn & 2) == 0)) { abc.text = "Intel AMT CIRA irrotettu"; addNotification(abc); }
                                    if (((node.conn & 4) != 0) && ((message.event.conn & 4) == 0)) { abc.text = "Intel AMT irrotettu"; addNotification(abc); }
                                    if (((node.conn & 16) != 0) && ((message.event.conn & 16) == 0)) { abc.text = "MQTT yhteys katkaistu"; 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>' + "Tätä osoitealuetta ei voi skannata." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Esimerkki IP-alueen arvoista" + '<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: llä."; } else { str += " ilman TLS: tä."; } } 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>' + "Scan ei tuottanut tuloksia." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Esimerkki IP-alueen arvoista" + '<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', "Ei mitään");
                                }
                            }
                            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 ei voinut viestiä tapahtumasta:", 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("Oikeudet tallennettu onnistuneesti");
                    } else {
                        alert("Virhe tallennettaessa käyttöoikeuksia: " + 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 tunti" : "{0} tuntia", message.expire);
                    if (message.expire == 24) { t = "1 päivä"; }
                    if (message.expire == 168) { t = "1 viikko"; }
                    if (message.expire == 5040) { t = "1 kuukausi"; }
                    if (message.expire == 0) { t = "Rajoittamaton"; }
                    QH('agentInvitationLink', format("Kutsulinkki ({0})", t));
                    QV('agentInvitationLinkDiv', true);
                    break;
                }
                case 'createDeviceShareLink': { // Guest sharing link
                    var node = getNodeFromId(message.nodeid), x = '';
                    if (node == null) break;
                    x += addHtmlValue("Laite", EscapeHtml(node.name));
                    x += addHtmlValue("Vieraan nimi", message.guestname);
                    x += addHtmlValue("Käyttäjän syöttö", message.viewOnly ? "Ei sallittu, vain katselu" : "Sallittu");
                    if (message.start && message.expire) {
                        if (message.recurring) {
                            x += addHtmlValue("Käynnistys aika", printDateTime(new Date(message.start)));
                            if (message.expire < 2) { x += addHtmlValue("Kesto", format("{0} minuutti", message.expire)); }
                            else { x += addHtmlValue("Kesto", format("{0} minuuttia", message.expire)); }
                        } else {
                            x += addHtmlValue("Käynnistys aika", printDateTime(new Date(message.start)));
                            x += addHtmlValue("Vanhentumisaika", printDateTime(new Date(message.expire)));
                        }
                    }
                    if (message.recurring == 1) { x += addHtmlValue("Toistuva", "Päivittäin"); }
                    if (message.recurring == 2) { x += addHtmlValue("Toistuva", "Viikoittain"); }
                    var y = [];
                    if (message.consent & 0x0007) { y.push("Ilmoita"); }
                    if (message.consent & 0x0038) { y.push("Kysy"); }
                    if (message.consent & 0x0040) { y.push("Tietosuojapalkki"); }
                    if (y.length == 0) { y.push("Ei mitään"); }
                    x += addHtmlValue("Käyttäjän suostumus", y.join(', '));
                    var type = ''; if (message.p <= 7) { type = ['', "Etäpäätelinkki", "Etätyöpöydän linkki", "Etätyöpöytä + päätelaite", "Linkki etätiedostoihin", "Etäpääte + Tiedostot -linkki", "Etätyöpöytä + tiedostot -linkki", "Etätyöpöytä + pääte + tiedostojen linkki"][message.p]; } else if (message.p == 8) { type = format("HTTP/{0} link", message.port); } else if (message.p == 16) { type = format("HTTPS/{0}", message.port); }
                    x += '<div id=agentInvitationLinkDiv style="text-align:center;font-size:large;margin:16px"><a href="' + message.url + '" id=agentInvitationLink rel="noreferrer noopener" target="_blank" style=cursor:pointer>' + type + '</a> <i class="fa-regular fa-copy" title="' + "Kopioi linkki leikepöydälle" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></i></div></div>';
                    setModalContent('xxAddAgent', "Jaa laite", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getmqttlogin': {
                    if ((currentNode == null) || (currentNode._id != message.nodeid) || (xxdialogMode != null)) return;
                    var x = "Näitä asetuksia voidaan käyttää MQTT: n liittämiseen tälle laitteelle." + '<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 Tunnukset", 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', "Laajennuksen toiminto", format('Select the version to downgrade the plugin: {0}', message.info.name) + '<hr />' + vSelect + '<hr />' + "Huomaa, että alentamista ei suositella. Tee niin vain siinä tapauksessa, että äskettäinen päivitys on rikkonut jotain." + + '<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>' + "Suurenna alla olevaan URL-osoitteeseen, myönnä käyttöoikeus ja kopioi tunnuskoodi takaisin." + '</div><br />';
                            x += addHtmlValue("Aktivointi", '<a href=\"' + message.url + '\" rel="noreferrer noopener" target="_blank">Browse to this URL</a>');
                            x += addHtmlFormFloating("Koodi", '<input id=gdcode onkeyup=server_setupGoogleDriveBackupCheck3() class=form-control></input>');
                            setModalContent('xxAddAgent', "Google Drive -varmuuskopio", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_setupGoogleDriveBackupEx(3, 'gd2'));
                            Q('gdcode').focus();
                            QE('idx_dlgOkButton', false);
                        }
                    }
                    break;
                }
                case 'pluginError': {
                    setModalContent('xxAddAgent', "Laajennuksen virhe", 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 = "Kelvollinen kirjautuminen"; c = 'BBD1BB'; xx = ''; if (message.events[i].tn != null) { m = format("Tunnus: {0}", message.events[i].tn); c = '88D188' } }
                            else if (m == 108) { m = "Virheellinen 2FA"; c = 'DD9DC3'; xx = 'x'; }
                            else if (m == 109) { m = "Lukittu tili"; c = 'E1BBBB'; xx = 'x'; }
                            else if (m == 110) { m = "Väärä salasana"; 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', "Edelliset kirjautumiset", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getDeviceDetails': {
                    saveAs(new Blob([message.data], { type: 'application/octet-stream' }), "laiteluettelo" + '.' + message.type);
                    break;
                }
                case 'createLoginToken': { // A new login token was created
                    if (xxdialogMode) return;
                    var x = "Muista tämä käyttäjätunnus ja salasana, salasanaa ei voida näyttää uudelleen." + '<br /><br />';
                    x += addHtmlValue("Nimi", EscapeHtml(message.name))
                    if (message.expire != 0) { x += addHtmlValue("Mennä umpeen", EscapeHtml(printDateTime(new Date(message.expire)))); }
                    x += addHtmlValue("Käyttäjätunnus", '<i class="fa-regular fa-copy" title="' + "Kopioi linkki leikepöydälle" + '" 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("Salasana", '<i class="fa-regular fa-copy" title="' + "Kopioi linkki leikepöydälle" + '" 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', "Kirjautumistunnus luotu", x);
                    QV('idx_dlgOkButton', false);
                    Q('idx_dlgCancelButton').innerHTML = "Sulje";
                    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' : '') + '>' + "Agentin tyyppi" + '</label><br />';
            x += '<label><input id=d2c9 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('agver') >= 0) ? ' checked' : '') + '>' + "Agentin versio" + '</label><br />';
            x += '<label><input id=d2c6 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('desc') >= 0) ? ' checked' : '') + '>' + "Laitekuvaus" + '</label><br />';
            x += '<label><input id=d2c5 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('os') >= 0) ? ' checked' : '') + '>' + "Käyttöjärjestelmä" + '</label><br />';
            x += '<label><input id=d2c10 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('tags') >= 0) ? ' checked' : '') + '>' + "Tunnisteet" + '</label><br />';
            x += '<label><input id=d2c11 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsav') >= 0) ? ' checked' : '') + '>' + "Windows AV" + '</label><br />';
            x += '<label><input id=d2c12 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) ? ' checked' : '') + '>' + "Windows Update" + '</label><br />';
            x += '<label><input id=d2c13 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) ? ' checked' : '') + '>' + "Windows Firewall" + '</label><br />';
            x += '<label><input id=d2c14 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) ? ' checked' : '') + '>' + "Last Boot Up Time" + '</label><br />';
            x += '<label><input id=d2c16 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('idletime') >= 0)?' checked':'') + '>' + "Lepotila" + '</label><br />';
            x += '<label><input id=d2c1 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('links') >= 0) ? ' checked' : '') + '>' + "MeshCentral -reitittimen linkit" + '</label><br />';
            x += '<label><input id=d2c2 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('user') >= 0) ? ' checked' : '') + '>' + "Kirjautuneet käyttäjät" + '</label><br />';
            x += '<label><input id=d2c3 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('ip') >= 0) ? ' checked' : '') + '>' + "Agentin IP -osoite" + '</label><br />';
            x += '<label><input id=d2c4 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('conn') >= 0) ? ' checked' : '') + '>' + "Palvelinyhteydet" + '</label><br />';
            x += '<label><input id=d2c15 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amthost') >= 0) ? ' checked' : '') + '>' + "Intel&reg; AMT hostname" + '</label><br />';
            x += '<label><input id=d2c17 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('amtstate') >= 0) ? ' checked' : '') + '>' + "Intel&reg; AMT state" + '</label><br />';
            x += '<label><input id=d2c7 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('lastseen') >= 0) ? ' checked' : '') + '>' + "Viimeksi nähty" + '</label><br />';
            setModalContent('xxAddAgent', "Laitteenäkymän sarakkeet", 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>' + ", vain Intel&reg; AMT" + '</span>'; }
                            if (meshes[node.meshid] && (meshes[node.meshid].mtype == 3)) { if (meshes[node.meshid].relayid) { extra = '<span class=devHeaderx>' + ", välitetyt laitteet" + '</span>'; } else { extra = '<span class=devHeaderx>' + ", Paikalliset laitteet" + '</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>' + "Yksittäiset laitteet" + '</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>' + "Yksittäiset laitteet" + '</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>' + "Yksittäiset laitteet" + '</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">' + "Laitteita ei sisälly mihinkään ryhmään. Napsauta laitteen \"Ryhmät\" lisätäksesi ryhmän." + '</div>';
                    } else {
                        r = '<div style="margin:30px">' + "Ei hakua vastaavia laitteita." + ' <a onclick=clearDeviceSearch()>' + "Tyhjennä hakusuodatin" + '</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>' + "Ei Intel&reg; AMT -laitteita tässä laiteryhmässä";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "lisää yksi" + '</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>' + "Tässä laiteryhmässä ei ole laitteita";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "lisää yksi" + '</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>' + "Tässä laiteryhmässä ei ole paikallisia laitteita";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "lisää yksi" + '</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>' + "Tässä laiteryhmässä ei ole laitteita";
                            }
                            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>' + "Agentin tyyppi"; }
                        if (deviceViewSettings.devsCols.indexOf('agver') >= 0) { colums += '<th style=width:100px>' + "Agentin versio"; }
                        if (deviceViewSettings.devsCols.indexOf('os') >= 0) { colums += '<th style=width:160px>' + "Käyttöjärjestelmä"; }
                        if (deviceViewSettings.devsCols.indexOf('tags') >= 0) { colums += '<th style=width:120px>' + "Tunnisteet"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsav') >= 0) { colums += '<th style=width:100px>' + "Windows AV"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsupdate') >= 0) { colums += '<th style=width:120px>' + "Windows Update"; }
                        if (deviceViewSettings.devsCols.indexOf('windowsfirewall') >= 0) { colums += '<th style=width:120px>' + "Windows Firewall"; }
                        if (deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) { colums += '<th style=width:120px>' + "Last Boot Up Time"; }
                        if (deviceViewSettings.devsCols.indexOf('idletime') >= 0) { colums += '<th style=width:100px title="' + "Päivitetty joka 5 minuutti" + '">' + "Lepotila"; }
                        if (deviceViewSettings.devsCols.indexOf('links') >= 0) { colums += '<th style=width:120px>' + "Linkit"; }
                        if(features3 & 0x00000002){
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:180px>' + "Käyttäjä"; }
                        } else {
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:120px>' + "Käyttäjä"; }
                        }
                        if (deviceViewSettings.devsCols.indexOf('ip') >= 0) { colums += '<th style=width:120px>' + "Osoite"; }
                        if (deviceViewSettings.devsCols.indexOf('conn') >= 0) { colums += '<th style=width:100px>' + "Liitettävyys"; }
                        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>' + "Viimeksi nähty";
                            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>' + "Tageja sisältäviä laitteita ei löytynyt." + '</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="' + "Luo uusi laiteryhmä." + '" style=cursor:pointer>' + "Lisää laiteryhmä" + '</a>&nbsp';
                }
                if ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 128) == 0)) {
                    r += '<a href=# onclick=\'return p10showMeshCmdDialog(0)\' style=cursor:pointer title="' + "Lataa MeshCmd, komentorivityökalu, joka suorittaa monia toimintoja." + '">' + "MeshCmd" + '</a>&nbsp';
                    if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) { r += '<a href=# onclick=\'return p10showMeshRouterDialog()\' style=cursor:pointer title="' + "Lataa MeshCentral Reititin, TCP-porttien tunnelointi työkalu." + '">' + "Reititin" + '</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, ["Yhteys katkaistu", "Yhdistetään...", "Asennus...", '', ''][((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, ["Yhteys katkaistu", "Yhdistetään...", "Asennus...", '', ''][((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>' + "Ei mitään" + '</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>' + "Ei mitään" + '</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 = "Laite on kytketty"; }
                    if (bat.state == 'dc') { statestr = "Laite on akkukäyttöinen"; }

                    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="' + "Kytkinportti on käyttövalmis." + '">' + "Vaihtaa" + '</span>');
                            } else {
                                states.push('<span title="' + "IP-KVM-portti on kytketty ja valmis käytettäväksi." + '">' + "IP-KVM" + '</span>');
                            }
                        } else {
                            states.push('<span title="' + "Mesh agentti on yhdistetty ja käyttövalmis." + '">' + "Agentti" + '</span>');
                        }
                    }
                    if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA yhdistetty ja valmis käytettäväksi." + '">' + "CIRA" + '</span>'); }
                    else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT on reititettävä." + '">' + "AMT" + '</span>'); }
                    if ((node.conn & 8) != 0) { states.push('<span title="' + "Verkkoagentti on tavoitettavissa käyttämällä toista ainetta releenä." + '">' + "Rele" + '</span>'); }
                    if ((node.conn & 16) != 0) { states.push('<span title="' + "MQTT-yhteys laitteeseen on aktiivinen." + '">' + "MQTT" + '</span>'); }
                }
                if (node.mtype == 3) {
                    var mesh = meshes[node.meshid];
                    if (mesh && mesh.relayid) {
                        states.push('<span title="' + "Paikallinen verkkoyhteys välitysagentin kautta." + '">' + "Rele" + '</span>');
                    } else {
                        states.push('<span title="' + "Paikallinen verkkoyhteys." + '">' + "Paikallinen" + '</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>' + "HUONO" + '</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>' + "HUONO" + '</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>' + "HUONO" + '</span>') : "");
                }
                if (deviceViewSettings.devsCols.indexOf('lastbootuptime') >= 0) { // Last Boot Up Time
                    r += '<td style=text-align:center;font-size:x-small>' + ((node.lastbootuptime != null) ? printDateTime(new Date(node.lastbootuptime)) : "");
                }
                if (deviceViewSettings.devsCols.indexOf('idletime') >= 0) { // Idle Time
                    r += '<td style=text-align:center;font-size:x-small>' + ((node.idletime != null && node.idletime != -1) ? printTimer(node.idletime) : "");
                }
                if (deviceViewSettings.devsCols.indexOf('links') >= 0) { r += '<td style=text-align:center;font-size:x-small>' + getShortRouterLinks(node); } // Links
                if (deviceViewSettings.devsCols.indexOf('user') >= 0) { r += '<td style=text-align:center>' + getUserShortStr(node); } // User
                if (deviceViewSettings.devsCols.indexOf('ip') >= 0) { var ip = ''; if (node.mtype == 3) { ip = node.host; } else if (node.ip) { ip = node.ip; } r += '<td style=text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis title="' + EscapeHtml(ip) + '">' + EscapeHtml(ip); } // IP address
                if (deviceViewSettings.devsCols.indexOf('conn') >= 0) { r += '<td style=text-align:center>' + states.join('&nbsp;+&nbsp;'); } // Connectivity
                if (deviceViewSettings.devsCols.indexOf('amthost') >= 0) { r += '<td style=text-align:center>' + (((node.intelamt == null) || (node.intelamt.host == null)) ? '' : EscapeHtml(node.intelamt.host)); }
                if (deviceViewSettings.devsCols.indexOf('amtstate') >= 0) {
                    var amtstate = '';
                    if (node.intelamt) {
                        if (node.intelamt.state == 0) { amtstate = ' <span title="' + "Intel&reg; AMT is not activated" + '">' + "Pre" + '</span>'; }
                        if (node.intelamt.state == 1) { amtstate = ' <span title="' + "Intel&reg; AMT is in activation mode" + '">' + "In" + '</span>'; }
                        if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { amtstate = ' <span title="' + "Intel&reg; AMT on aktivoitu asiakasohjaustilassa" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { amtstate += ' <span title="' + "Intel&reg; AMT on aktivoitu Järjestelmänvalvojan ohjaustilassa" + '">' + "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 += "Yhdistetty"; } 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 + '\')">' + "Yhteys katkaistu" + '</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) + '")>' + "Hylkää" + '</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', "Apupyynnöt" + ' - ' + 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>' + "Etätyöpöytä" + '</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="' + "Katkaise tämän istunnon yhteys" + '" class="fa-fw fa-solid fa-trash text-danger" ></i>'; }
                            x += addHtmlValue2(getUserName(j), ((node.sessions.kvm[j] == 1) ? "1 istunto" : nobreak(format("{0} istuntoa", node.sessions.kvm[j]))) + trash);
                        } else if (j == 'busy') {
                            x += addHtmlValue2("Laite on varattu", ((node.sessions.kvm[j] == 1) ? "1 istunto" : nobreak(format("{0} istuntoa", node.sessions.kvm[j]))));
                        }
                    }
                } else if (i == 'multidesk') {
                    x += '<u>' + "Etätyöpöytä" + '</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="' + "Katkaise tämän istunnon yhteys" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.multidesk[j] == 1) ? "1 istunto" : nobreak(format("{0} istuntoa", node.sessions.multidesk[j]))) + trash);
                    }
                } else if (i == 'terminal') {
                    x += '<u>' + "Pääte" + '</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="' + "Katkaise tämän istunnon yhteys" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.terminal[j] == 1) ? "1 istunto" : nobreak(format("{0} istuntoa", node.sessions.terminal[j]))) + trash);
                    }
                } else if (i == 'files') {
                    x += '<u>' + "Tiedostot" + '</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="' + "Katkaise tämän istunnon yhteys" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.files[j] == 1) ? "1 istunto" : nobreak(format("{0} istuntoa", node.sessions.files[j]))) + trash);
                    }
                } else if (i == 'tcp') {
                    x += '<u>' + "TCP-reititys" + '</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="' + "Katkaise tämän istunnon yhteys" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.tcp[j] == 1) ? "1 istunto" : nobreak(format("{0} istuntoa", node.sessions.tcp[j]))) + trash);
                    }
                } else if (i == 'udp') {
                    x += '<u>' + "UDP-reititys" + '</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="' + "Katkaise tämän istunnon yhteys" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.udp[j] == 1) ? "1 istunto" : nobreak(format("{0} istuntoa", node.sessions.udp[j]))) + trash);
                    }
                }
            }
            if (x != '') {
                xxdialogTag = 'SESSIONS-' + node._id;
                setModalContent('xxAddAgent', "Istuntoja" + ' - ' + 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', "Agenttiviestit" + ' - ' + 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} käyttäjää", 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) + ' (' + "Lukittu" + ')', (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', "Yhdistä kaikkiin", format("Haluatko varmasti muodostaa yhteyden {0} laitteeseen?", 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, ["Yhteys katkaistu", "Yhdistetään...", "Asennus...", '', ''][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', "Etätyöpöydän asetukset", 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="' + "Lisää uusi Intel&reg; AMT-tietokone, joka sijaitsee lähiverkossa." + '" onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "Lisää paikallinen" + '</a>';
                    r += ' <a href=# role="button" title="' + "Lisää uusi Intel&reg; AMT-tietokone skannaamalla lähiverkko." + '" onclick=\'return addAmtScanToMesh("' + mesh._id + '")\'>' + "Skannaa verkko" + '</a>';
                }
                if (mesh.amt && (mesh.amt.type > 0)) { // CCM Deactivate, CCM or ACM activation, Full Automatic
                    r += ' <a href=# role="button" title="' + "Suorita Intel&reg; AMT -aktivointi ja -määritykset." + '" onclick=\'return showAmtSetup("' + mesh._id + '")\'>' + "Perustaa" + '</a>';
                }
            }
            if ((mesh.mtype == 2) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { // Agent device group
                r += ' <a href=# role="button" title="' + "Lisää uusi tietokone tähän laiteryhmään asentamalla MeshAgent." + '" onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "Lisää agentti" + '</a>';
                if ((features & 2) == 0) { r += ' <a href=# role="button" title="' + "Kutsu joku asentamaan Mesh agentti tähän laiteryhmään." + '" onclick=\'return inviteAgentToMesh("' + mesh._id + '")\'>' + "Kutsu" + '</a>'; }
            }
            if (mesh.mtype == 3) { // Local device group
                r += ' <a href=# role="button" title="' + "Lisää laite, joka sijaitsee paikallisessa verkossa." + '" onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "Lisää Laite" + '</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("Lisää paikallinen laite laiteryhmään \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Laitteen Nimi", '<input id=dp1devicename class=form-control maxlength=32 autocomplete=off onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Isäntänimi", '<input id=dp1hostname class=form-control maxlength=32 autocomplete=off placeholder="' + "Sama kuin laitteen nimi" + '" onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Tyyppi", '<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', "Lisää paikallinen laite", 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("Lisää uusi Intel&reg; AMT -laite laiteryhmään \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Laitteen Nimi", '<input id=dp1devicename class="form-control" maxlength=32 autocomplete=off onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Isäntänimi", '<input id=dp1hostname class="form-control" maxlength=32 autocomplete=off placeholder="' + "Sama kuin laitteen nimi" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Käyttäjätunnus", '<input id=dp1username class="form-control" maxlength=32 autocomplete=off placeholder="' + "pääkäyttäjä" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Salasana", '<input id=dp1password type=password class="form-control" autocomplete=off maxlength=32 onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Turvallisuus", '<select id=dp1tls class="form-select"><option value=0>' + "Ei TLS-suojausta" + '</option><option value=1>' + "TLS-suojaus vaaditaan" + '</option></select>');

            setModalContent('xxAddAgent', "Lisää Intel&reg; AMT laite", 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 = "Create many Intel&reg; AMT device at once by importing a JSON file with the following format:" + '<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 Device Import", 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 Device Import", format("JSON-tiedosto {0} on virheellinen.", ex));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    return;
                }
                if (j != null) {
                    meshserver.send({ action: 'importamtdevices', meshid: meshid, amtdevices: j });
                } else {
                    setModalContent('xxAddAgent', "Intel&reg; AMT Device Import", "Virheellinen JSON-tiedostomuoto.");
                    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("Lisää, aktivoi ja määritä Intel&reg; AMT ryhmittelemään \"{0}\" suorittamalla ajoittain MeshCmd etälaitteen järjestelmänvalvojana.", 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-aktivointia varten Intel&reg; AMT on asetettava seuraavaan luotettavaan FQDN: ään:" + ' <b>' + serverinfo.amtAcmFqdn.join(', ') + '</b></div>');
            }
            setModalContent('xxAddAgent', "Intel&reg; AMT -asennus", 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>' + "Aktivoi Intel&reg; AMT järjestelmänvalvojan ohjaustilassa (ACM) FAT-muotoisella USB-avaimella. Aseta setup.bin siihen ja käynnistä yksi tai useampi tietokone tällä avaimella." + '</div><div style=margin-top:6px>' + "Aloita kirjoittamalla vanha ja uusi MEBx-salasana." + '</div></table>';
            x += addHtmlFormFloating("vanha salasana", '<input id=dp1password0 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Uusi salasana*", '<input id=dp1password1 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Uusi salasana*", '<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" /> ' + "Käytä paljaalle metallille lähiverkkoaktivointiin." + '</label>'; } // Intel AMT LAN provisioning server is active.
            x += '<div><span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 merkkiä, 1 ylempi, 1 alempi, 1 numeerinen, 1 ei-aakkosnumeerinen." + '</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 = "Syötä IP-alue, josta etsitään Intel&reg; AMT-laitteita." + '<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 Alue", '<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="' + "Skannaa" + '" onclick=addAmtScanToMeshButton()></input></tr></table>');
            } else {
                x += addHtmlValue("IP Alue", '<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="' + "Skannaa" + '" 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="' + "Peruuta" + '" style="" onclick="dialogclose(0)" /><input id="idx_dlgOkButton2" type="button" value="OK" style="" onclick="dialogclose(1)" /><div><input id="d2scanSelectButton" type="button" value="' + "Valitse kaikki" + '" onclick="scanSelectAll()" /></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Skannaa Intel&reg; AMT laitteita", 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>' + "Esimerkki IP-alueen arvoista" + '<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>' + "Skannataan..." + '</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("Kutsutyyppi", '<select id=d2InviteType onchange=d2ChangedInviteType() class=form-select><option value=0>' + "Kutsu linkillä" + '</option><option value=1>' + "Lähetä kutsu sähköpostitse" + '</option></select>') + '<hr />';
                x += '<div id=emailInviteDiv style=display:none>' + format("Kutsu joku asentamaan verkkoagentti. Lähetettävä sähköposti ja linkki verkkoagenttiasennukseen laiteryhmälle \"{0}\".", EscapeHtml(mesh.name)) + '<br /><br />';
                x += addHtmlFormFloating("Nimi (valinnainen)", '<input id=agentInviteName value="" placeholder="Name (optional)" class="form-control" maxlength=64 />');
                x += addHtmlFormFloating("Sähköposti", '<input id=agentInviteEmail placeholder="' + "matti.meikäläinen@sahkoposti.fi" + '" class="form-control" onkeyup=validateAgentInvite() />');
                x += addHtmlFormFloating("Käyttöjärjestelmä", '<select id=agentInviteNameOs onchange=d2ChangedInviteType() class=form-select><option value=4>' + "Lähetä asennus linkki" + '</option><option value=0 selected>' + "Kaikki tuetut" + '</option><option value=1>' + "Vain Windows" + '</option><option value=3>' + "Vain Apple macOS" + '</option><option value=2>' + "Vain Linux" + '</option><option value=5>' + "MeshCentral Assistant" + '</option></select>');
                x += '<div id=d2agentexpirediv>';
                x += addHtmlFormFloating("Linkin vanheneminen", '<select id=agentInviteExpire class=form-select><option value=1>' + "1 tunti" + '</option><option value=8>' + "8 tuntia" + '</option><option value=24>' + "1 päivä" + '</option><option value=168>' + "1 viikko" + '</option><option value=5040>' + "1 kuukausi" + '</option><option value=0>' + "Rajoittamaton" + '</option></select>');
                x += '</div>';
                x += '<div id=d2agentInstallTypeDiv2>';
                x += addHtmlFormFloating("Asennustyyppi", '<select id=agentInviteType class=form-select><option value=0>' + "Tausta ja vuorovaikutteinen" + '</option><option value=2>' + "Vain tausta" + '</option><option value=1>' + "Vain interaktiivinen" + '</option></select>');
                x += '</div>';
                x += addHtmlFormFloating("Viesti" + ' ' + "(valinnainen)", '<textarea id="agentInviteMessage" class="form-control" style="height:100px;resize:none;" maxlength="1024"></textarea>');
                x += '</div>';
            }
            x += '<div id=urlInviteDiv>' + format("Kutsu joku asentamaan verkkoagentti jakamalla kutsulinkki. Tämä linkki ohjaa käyttäjän \"{0}\" laiteryhmän asennusohjeisiin. Linkki on julkinen, eikä tälle palvelimelle tarvita tiliä.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Linkin vanheneminen", '<select id=d2inviteExpire class=form-select onchange=d2RequestInvitationLink()><option value=1>' + "1 tunti" + '</option><option value=8>' + "8 tuntia" + '</option><option value=24>' + "1 päivä" + '</option><option value=168>' + "1 viikko" + '</option><option value=5040>' + "1 kuukausi" + '</option><option value=0>' + "Rajoittamaton" + '</option></select>');
            x += addHtmlFormFloating("Agentit", '<select id=d2agentType class=form-select onchange=d2ChangedInviteType()><option value=0>' + "Kaikki käytettävissä olevat edustajat" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "MeshCentral Assistant" + '</option><option value=16>' + "Android MeshAgent" + '</option></select>');
            x += '<div id=d2agentInstallTypeDiv>';
            x += addHtmlFormFloating("Asennustyyppi", '<select id=d2agentInviteType class=form-select onchange=d2RequestInvitationLink()><option value=0>' + "Tausta ja vuorovaikutteinen" + '</option><option value=2>' + "Vain tausta" + '</option><option value=1>' + "Vain interaktiivinen" + '</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="' + "Kopioi linkki leikepöydälle" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Kutsu", 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 -binaariasennusohjelma" + '</option><option value=2>' + "Apple macOS" + '</option>';
            if ((features & 2) == 0) { opts += '<option value=6>' + "Mobiililaite" + '</option>'; } // Don't display mobile setup in LAN mode.
            opts += '<option value=7>' + "MeshCentral Assistant" + '</option>';
            opts += '<option value=3>' + "Windows (Poista asennus)" + '</option><option value=4>' + "Linux / BSD (Asennuksen poisto)" + '</option><option value=8>' + "Apple macOS (Uninstall)" + '</option></select>';
            x += addHtmlFormFloating("Käyttöjärjestelmä", 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("Järjestelmän tyyppi", '<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("Asennustyyppi", '<select id="aginsType" onchange="addAgentToMeshClick()" class="form-select"><option value="0">' + "Tausta & vuorovaikutteinen" + '</option><option value="2">' + "Vain tausta" + '</option><option value="1">' + "Vain interaktiivinen" + '</option></select>');
            x += '</div><div id="asinsTypeDiv">';
            x += addHtmlFormFloating("Asennustyyppi", '<select id="asinsType" onchange="addAgentToMeshClick()" class="form-select"><option value="2">' + "Sovellus, Yhdistä käyttäjän pyynnöstä" + '</option><option value="3">' + "Sovellus, aina yhteydessä" + '</option><option value="0">' + "Järjestelmälokero, yhdistä käyttäjän pyynnöstä" + '</option><option value="1">' + "Järjestelmälokero, aina kytketty" + '</option><option value="4">' + "Järjestelmälokero, vain näyttö" + '</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("Jos haluat lisätä uuden tietokoneen laiteryhmään \"{0}\", lataa verkkoagentti ja asenna se tietokone hallittavaksi. Tällä agentilla on palvelimen ja laiteryhmän tietoja.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline id=aginsw32lnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 32bit version of the MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copy Windows x86 32bit agent URL to clipboard" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline id=aginsw64lnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 64bit version of the MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copy Windows x86 64bit agent URL to clipboard" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline id=aginswa64lnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "ARM 64bit version of the MeshAgent" + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copy Windows ARM 64bit agent URL to clipboard" + '" 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("Asetukset-tiedosto", '<a id=aginswmshlnk name="meshsettings?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + format("{0} asetukset (.msh)", EscapeHtml(mesh.name)) + '</a>'); }
            x += '</div>';

            // Linux agent install
            x += '<div id=agins_linux style=display:none>' + format("Lisää tietokone \"{0}\" suorittamalla seuraava komento. Tarvitaan juuretunnuksia.", 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>' + "* BSDtä käyttäessa suorita ensin \"pkg install wget sudo bash\"" + '</div><a style=text-decoration:none title="' + "Kopioi leikepöydälle" + '" onclick=copyAgentIdValue("agins_linux_area")>' + "Kopioi" + ' <i class="fa-regular fa-clipboard" style=cursor:pointer></i></a></div>';

            // macOS agent install
            x += '<div id=agins_osx style=display:none>' + format("Jos haluat lisätä uuden tietokoneen laiteryhmään \"{0}\", lataa verkkoagentti ja asenna se tietokone hallittavaksi. Tämä agentti-asennusohjelma sisältää palvelimen ja laiteryhmän tiedot.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Mesh Agentti", '<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-bittinen" + '</a> <i class="fa-regular fa-clipboard" title="' + "Kopioi macOS-agentin URL-osoite leikepöydälle" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=16&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agentti", '<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-bittinen)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Kopioi macOS-agentin URL-osoite leikepöydälle" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=29&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Universal version of macOS Mesh Agent" + '">' + "macOS (Universal)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Kopioi macOS-agentin URL-osoite leikepöydälle" + '" 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("Jos haluat lisätä mobiililaitteen ryhmään \"{0}\", lataa MeshAgent -sovellus ja skannaa tämä QR -koodi.", EscapeHtml(mesh.name)) + '</div>';
            x += '<table style=width:180px>';
            x += '<tr><td style=text-align:center><a title="' + "Google Play Kauppa" + '"rel="noreferrer noopener" target=_blank href="https://play.google.com/store/apps/details?id=com.meshcentral.agent2"><img style=cursor:pointer src="images/google-play-140.png" width=140 srcset="images/google-play-280.png 2x" /></a></td></tr>';
            x += '<tr><td style=text-align:center><a title="' + "Amazon App Store" + '" rel="noreferrer noopener" target=_blank href="https://www.amazon.co.uk/gp/product/B097Z4Q7SK/"><img style=cursor:pointer src="images/amazon-appstore-140.png"  width=140 srcset="images/amazon-appstore-280.png 2x" /></a></td></tr>';
            x += '</table>';
            x += addHtmlValue("Android APK", '<a class=text-decoration-underline onclick=downloadFile("meshagents?id=14' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '",null,true) title="' + "APK version of the MeshAgent" + '">' + "APK" + '</a> <i class="fa-regular fa-clipboard" title="' + "Kopioi URL -osoite leikepöydälle" + '" 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 on Windows -työkalu, jonka avulla käyttäjät voivat pyytää apua. Lataa alla olevasta linkistä versio, joka muodostaa yhteyden laiteryhmään \"{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 Windowsille" + '">' + "Assistant Windowsille (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Kopioi URL -osoite leikepöydälle" + '" 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 on Windows -työkalu, jonka avulla käyttäjät voivat pyytää apua. Käytä alla olevaa linkkiä ladataksesi version, joka seuraa taustaagenttia." + '</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 Windowsille" + '">' + "Assistant Windowsille (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Kopioi URL -osoite leikepöydälle" + '" 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>' + "Voit poistaa verkkoagentin lataamalla alla olevan tiedoston, suorittamalla sen ja napsauttamalla \"Poista \"." + '<br /><br />';
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline id=aginsw32unlnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 32bit version of the MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copy Windows x86 32bit agent URL to clipboard" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline id=aginsw64unlnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 64bit version of the MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copy Windows x86 64bit agent URL to clipboard" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline id=aginswa64unlnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "ARM 64bit version of the MeshAgent" + '">' + "Windows ARM-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Copy Windows ARM 64bit agent URL to clipboard" + '" 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>' + "Poista verkkoagentti suorittamalla seuraava komento. Tarvitaan juuretunnuksia." + '<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="' + "Kopioi leikepöydälle" + '" 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>' + "Voit poistaa verkkoagentin lataamalla alla olevan tiedoston, purkamalla ZIP-arkiston ja napsauttamalla hiiren kakkospainikkeella \"Uninstall.command\" ja valitsemalla \"Avaa\"." + '<br /><br />';
            x += addHtmlValue("Mesh Agentti", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Universal version of macOS Mesh Agent" + '">macOS Agent (Universal)</a> <i class="fa-regular fa-clipboard" title="' + "Kopioi macOS-agentin URL-osoite leikepöydälle" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += '</div>';

            // Linux binary installer
            x += '<div id=agins_linux_inst style=display:none>' + "This is an executable on OSs with graphical user interfaces." + '<br /><br />' + "Apple macOS executables will need to be removed from quarantine by running 'xattr -r -d com.apple.quarantine meshagent'." + '<br /><br />' + "You need to run 'chmod +x meshagent', and then run this file." + '<br /><br />';
            x += addHtmlValue("Mesh Agentti", '<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="' + "Kopioi agentin URL-osoite leikepöydälle" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Komento" + '&nbsp;<i class="fa-regular fa-clipboard" title="' + "Kopioi agentin URL-osoite leikepöydälle" + '" 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', "Lisää 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 solmu" : format("{0} solmua", 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="' + "Laite on päällä." + '">' + "Käynnissä" + '</span>', '<span title="' + "Laite on lepotilassa (S1)." + '">' + "Nukkumassa" + '</span>', '<span title="' + "Laite on lepotilassa (S2)." + '">' + "Nukkumassa" + '</span>', '<span title="' + "Laite on syvän unen tilassa (S3)." + '">' + "Syvä lepotila" + '</span>', '<span title="' + "Laite on horros tilassa (S4)." + '">' + "Horroksessa" + '</span>', '<span title="' + "Laite on pois päältä-tilassa (S5)." + '">' + "Pehmeä pois päältä" + '</span>', '<span title="' + "Laite havaitaan, mutta virran tilaa ei voitu saada." + '">' + "Läsnä" + '</span>', '<span title="' + "Laitteen virta on katkaistu." + '">' + "Vinossa" + '</span>'];
        var powerStateStrings2 = ['', "Laite saa virtaa", "Laite on lepotilassa (S1)", "Laite on lepotilassa (S2)", "Laite on syvän unen tilassa (S3)", "Laite on horroksessa (S4)", "Laite on pois päältä-tilassa (S5)", "Laite on läsnä, mutta virran tilaa ei voida määrittää", "Laitteesta on katkaistu virta"];
        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="' + "Virtakytkin on käyttövalmis." + '">' + "Vaihtaa" + '</span>');
                        } else {
                            states.push('<span title="' + "IP-KVM-portti on valmis ja valmis käytettäväksi." + '">' + "IP-KVM" + '</span>');
                        }
                    } else {
                        states.push('<span title="' + "Mesh agentti on yhdistetty ja käyttövalmis." + '">' + "Agentti" + '</span>');
                    }
                }
                if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA yhdistetty ja valmis käytettäväksi." + '">' + "CIRA" + '</span>'); }
                else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT on reititettävä." + '">' + "AMT" + '</span>'); }
                if ((node.conn & 8) != 0) { states.push('<span title="' + "Verkkoagentti on tavoitettavissa käyttämällä toista ainetta releenä." + '">' + "Rele" + '</span>'); }
                if ((node.conn & 16) != 0) { states.push('<span title="' + "MQTT-yhteys laitteeseen on aktiivinen." + '">' + "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 "Tuntematon";
        }

        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 = "Poista valinta";
                QV('cxmdesktop', true);
            } else {
                QE('GroupActionButton', false);
                Q('SelectAllButton').value = "Valitse kaikki";
                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', "Tilin Turvallisuus", "Tätä ominaisuutta ei voi käyttää, ennen kuin kaksivaiheinen todennus on käytössä. Tätä tarvitaan ylimääräisen turvallisuuden takaamiseksi. Siirry \"Oma tili\" -välilehdelle ja katso \"Tilin suojaus\" -osio.");
                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: "Lähetä MQTT Viesti" });; break; } }
            }

            // Display the options that are allowed depending on what devices are selected.
            addedOptions.push({ v: 105, name: "Vie laitetiedot", 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: "Siirrä laiteryhmään" }); }
                if (((node.conn & 1) != 0) && ((rights & 0x8000) != 0) && ((added & 1) == 0)) { added |= 1; addedOptions.push({ v: 104, name: "Agentin asennuksen poistaminen" }); }
                if ((rights & 64) && ((added & 8) == 0)) { added |= 8; addedOptions.push({ v: 100, name: "Herätä laitteet" }); }
                if ((rights & 262144) && ((added & 4) == 0)) { added |= 4; addedOptions.push({ v: 4, name: "Lepotilat" }); addedOptions.push({ v: 3, name: "Nollaa laitteet" }); addedOptions.push({ v: 2, name: "Sammuta laitteet" }); }
                if ((rights & 131072) && ((added & 16) == 0)) { added |= 16; addedOptions.push({ v: 106, name: "Suorita komentoja" }); }
                if ((rights & 16384) && ((added & 32) == 0)) { added |= 32; addedOptions.push({ v: 108, name: "Laite-ilmoitus" }); }
                if ((rights & 4) && ((added & 64) == 0)) { added |= 64; addedOptions.push({ v: 107, name: "Muokkaa tunnisteita" }); }
                if ((rights & 8) && ((added & 256) == 0)) { added |= 256; addedOptions.push({ v: 109, name: "Lähetä tiedostoja" }); }
                if ((rights & 32768) && ((added & 128) == 0)) { added |= 128; addedOptions.push({ v: 101, name: "Poista laitteet" }); }
                if ((node.agent != null) && (features2 & 0x00000010) && (rights == 0xFFFFFFFF) && ((added & 512) == 0)) {
                    added |= 512;
                    addedOptions.push({ v: 110, name: "Pakota agenttipäivitys" });
                    addedOptions.push({ v: 111, name: "Kirkas agenttiydin" });
                    addedOptions.push({ v: 112, name: "Lähetä palvelimen oletusydin" });
                }
            }

            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 = "Valitse suoritettava toiminto kaikille valituille laitteille. Toiminnot suoritetaan vain oikeilla oikeuksilla." + '<br /><br />';
            x += addHtmlFormFloating("Operaatio", '<select id=d2groupop class="form-select">' + addedOptionsStr + '</select>');

            setModalContent('xxAddAgent', "Ryhmätoiminta", 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 += "Vahvistetaanko valitun laitteen poisto?" + '<br /><br />'; }
                else {
                    x += format("Vahvistetaanko {0} valitun laitteen poisto?" + '<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 valittu laite on verkossa." + '<br />', nconn); }
                    else if (nconn > 1) { y += format("{0} valittua laitetta on verkossa." + '<br />', nconn); }
                    if (ndisc == 1) { y += format("1 valittu laite on offline -tilassa." + '<br />', ndisc); }
                    else if (ndisc > 1) { y += format("{0} valittua laitetta on offline -tilassa." + '<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() />' + "Varmista" + '</label>';
                setModalContent('xxAddAgent', "Poista laitteet", 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: "Suorita komentoja valituilla laitteilla.", func: uncheckAllDevices });
            } else if (op == 107) {
                // Edit tags
                var x = "Suorita erälaitetunniste" + '<br /><br />';
                x += addHtmlFormFloating("Operaatio", '<select id=d2deviceop class="form-select"><option value=1>' + "Lisää tageja" + '</option><option value=2>' + "Aseta tunnisteet" + '</option><option value=3>' + "Poista tunnisteet" + '</option></select>');
                x += addHtmlFormFloating("Tunnisteet", '<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', "Muokkaa laitetunnisteita", x);
                $('#dp10devicevalue').select2({
                    theme: 'bootstrap-5',
                    width: $( this ).data( 'width' ) ? $( this ).data( 'width' ) : $( this ).hasClass( 'w-100' ) ? '100%' : 'style',
                    placeholder: "Tag1, Tag2, Tag3",
                    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>'+ "Suorita erälaite-ilmoitus" + '</div>';
                x += '<select id=d2deviceop class="form-select-sm me-2" style=width:100%;margin-bottom:4px><option value=2>' + "Toast-ilmoitus" + '</option><option value=1>' + "Viestilaatikko" + '</option><option value=3>' + "Alert Box" + '</option></select>';
                x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Otsikko" + '" style=width:100%;box-sizing:border-box;margin-bottom:4px />';
                x += '<textarea id=d2notifyMsg style=width:100%;height:140px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px></textarea>';
                x += '<select style=width:100% id=d2notifyTimeout>';
                x += '<option disabled value="">Only Applicable to Message Box Notifications</option>';
                x += '<option value=2 selected>' + "Show for 2 Minutes (Default)" + '</option>';
                x += '<option value=10>' + "Show for 10 minutes" + '</option>';
                x += '<option value=30>' + "Show for 30 minutes" + '</option>';
                x += '<option value=60>' + "Show for 60 minutes" + '</option>';
                x += '<option value=0>' + "Näytä viesti, kunnes käyttäjä hylkäsi sen" + '</option>';
                x += '</select>';
                setModalContent('xxAddAgent', "Laiteilmoitus", 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 = "Lähetä valitut tiedostot kaikkiin valittuihin laitteisiin" + '<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("Windowsin polku", '<input type=text class="form-control" onchange="d2batchUploadValidate()" onkeyup="d2batchUploadValidate()" name=winpath id=d2winuploadpath placeholder="C:\\temp" value="" />'); }
                if (linuxtype) { x += addHtmlFormFloating("Linux-polku", '<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 />' + "Luo kansio, jos sitä ei ole olemassa?" + '</label><br />';
                x += '<label><input type=checkbox class="form-check-input me-2" name=overwriteFiles />' + "Korvataanko tiedosto olemassa?" + '</label></form>';
                setModalContent('xxAddAgent', "Erätiedoston lataus", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2batchUploadValidateOk());
                d2batchUploadValidate();
            } else if (op == 110) {
                // Force agent update
                var x = "Pakotetaanko agenttipäivitys valituille laitteille?" + '<br /><br />';
                setModalContent('xxAddAgent', "Pakota agenttipäivitys", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentUpdateExec());
            } else if (op == 111) {
                // Clear agent core
                var x = "Tyhjennätkö agentin ytimen valituilla laitteilla?" + '<br /><br />';
                setModalContent('xxAddAgent', "Kirkas agenttiydin", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentClearCoreExec());
            } else if (op == 112) {
                // Upload default server core
                var x = "Lähetetäänkö oletuspalvelinydin valituille laitteille?" + '<br /><br />';
                setModalContent('xxAddAgent', "Lähetä palvelimen oletusydin", 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 = "Lataa luettelo laitteista, jollakin alla olevista tiedostomuodoista." + '<br /><br />';
            x += addHtmlValue("CSV muoto", '<a href=# style=cursor:pointer onclick=\'return p2downloadDeviceInfoCSV()\'>' + 'devicelist.csv' + '</a>');
            x += addHtmlValue("JSON Muoto", '<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 />' + "Sisällytä laitteen tiedot" + '</label></div>';
            setModalContent('xxAddAgent', "Laitetietojen vienti", 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' : '') + '>' + "Windowsin komentohehote" + '</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 komentokehote" + '</option>'; }
                if (agenttype == true) { x += '<option value=4' + ((runopt.type == 4) ? ' selected' : '') + '>' + "Agentin konsoli" + '</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' : '') + '>' + "Suorita agenttina" + '</option><option value=1' + ((runopt.runAs == 1) ? ' selected' : '') + '>' + "Suorita käyttäjänä, agenttina, jos ei käyttäjää" + '</option><option value=2' + ((runopt.runAs == 2) ? ' selected' : '') + '>' + "On suoritettava käyttäjänä" + '</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' : '') + '>' + "Commands from text box" + '</option><option value=1' + ((runopt.source == 1) ? ' selected' : '') + '>' + "Commands from file" + '</option>';
                    if (userinfo.siteadmin & 8) { x += '<option value=2' + ((runopt.source == 2) ? ' selected' : '') + '>' + "Commands from file on server" + '</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', "Suorita Komentoja", 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(' ' + "ja" + ' '), 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(' ' + "tai" + ' '), 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-etäyhteysportti:" + '<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 Yhteys", 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 -etäyhteysportti:" + '<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 -yhteys", 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 -etäyhteysportti:" + '<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 -liitäntä", 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-etäyhteysportti:" + '<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-yhteys", 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-etäyhteysportti:" + '<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-yhteys", 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', "Nimeä uudelleen", '<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("Tiedostoeditori", "Vain alle 200 kt: n tiedostoja voidaan muokata."); }
            } else if (action == 3) {
                // Delete the file
                setModalContent('xxAddAgent', "Poista", "Poista kohde?");
                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: "Muokkaa solmun sijaintia", callback: function (obj) { modifyMarkerloc(obj.data); } };

            // Clear Marker item
            map_cm_clearMarker = {
                text: "Poista solmun sijainti", callback: function (obj) {
                    meshserver.send({ action: 'changedevice', nodeid: obj.data.a, userloc: [] }); // Clear the user position marker
                }
            };

            // Save Marker item
            map_cm_saveMarker = { text: "Tallenna solmun sijainti", callback: function (obj) { saveMarkerloc(obj.data); } };

            // Build a context menu for a feature
            map_cm_nodemenu_items = [
                { text: "Yleistä tietoa", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 10); } } },
                { text: "Työpöytä", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 11); } } },
                { text: "Pääte", 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: "Lähennä laajuutta", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 19); } },
                { text: "Loitonna laajuudessa", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 2); } }
            ];

            // Context menu for clicks other than on feature
            contextmenu_items = [
                { text: "Päivitä", callback: function () { refreshMap(true, true); } },
                { text: "Zoomaa sopivaksi", callback: function () { zoomToFitExtent(); } },
                { text: "Keskitä kartta tähän", callback: function (obj) { xxmap.mapView.animate({ center: obj.coordinate }); } },
                { text: "Aseta solmu tähän", 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 == "Lähennä laajuutta" || item.text == "Loitonna laajuudessa") { 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>' + "Etsi" + '</label>&nbsp&nbsp<input type=text placeholder="' + "Laitteen nimi" + '" 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>' + "Laitteita ei löytynyt." + '</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', "Valitse sijoitettava solmu", 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>' + "Sijaintia ei löytynyt." + '<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', "Tilin Turvallisuus", "Tätä ominaisuutta ei voi käyttää, ennen kuin sähköpostiosoite on vahvistettu. Tätä vaaditaan salasanan palauttamiseksi. Siirry \"Oma tili\" -välilehteen vaihtaaksesi ja vahvistaaksesi sähköpostiosoitteen.");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return;
            }

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0)) {
                setModalContent('xxAddAgent', "Tilin Turvallisuus", "Tätä ominaisuutta ei voi käyttää, ennen kuin kaksivaiheinen todennus on käytössä. Tätä tarvitaan ylimääräisen turvallisuuden takaamiseksi. Siirry \"Oma tili\" -välilehdelle ja katso \"Tilin suojaus\" -osio.");
                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 = "Laite on kytketty"; }
                    if (bat.state == 'dc') { statestr = "Laite on akkukäyttöinen"; }

                    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>' + "Ei mitään" + '</i>'; }
                if (((meshrights & 4) != 0) && ((!mesh.flags) || ((mesh.flags & 2) == 0 || (mesh.flags & 16)))) { nname = '<span tabindex=0 title="' + "Napsauta tätä muokataksesi palvelinpuolen laitteen nimeä" + '" 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("Nimi", nnameEx); }

                // Attribute: Mesh
                if (mesh) { x += addDeviceAttribute('<span title="' + "Laiteryhmän nimi, johon tämä tietokone kuuluu." + '">' + "Ryhmä" + '</span>', '<a href=# title="' + "Laiteryhmän nimi, johon tämä tietokone kuuluu" + '" 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="' + "Tämän tietokoneen nimi käyttöjärjestelmässä määritetyllä tavalla" + '">' + "OS-nimi" + '</span>', '<span title="' + "Tämän tietokoneen nimi käyttöjärjestelmässä määritetyllä tavalla" + '">' + 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("Isäntänimi", addLinkConditional((node.host ? EscapeHtml(node.host) : ('<i>' + "Ei mitään" + '</i>')), 'showEditNodeValueDialog(1)', meshrights & 4));
                }

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

                // IP-KVM / PDU information
                if (node.mtype == 4) {
                    if (node.portnum != null) { x += addDeviceAttribute("Porttinumero", node.portnum); }
                    if (node.porttype != null) { x += addDeviceAttribute("Portin tyyppi", node.porttype); }
                }

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

                // Attribute: Intel AMT
                if (node.intelamt != null) {
                    var str = '';
                    var provisioningStates = { 0: nobreak("Ei aktivoitu (ennakko)"), 1: nobreak("Ei aktivoitu (sisään)"), 2: nobreak("Aktivoitu") };
                    if (node.intelamt.ver != null && node.intelamt.state == null) { str += '<i>' + "Tuntematon tila" + '</i>, v' + EscapeHtml(node.intelamt.ver); }
                    else if ((node.intelamt.ver == null) && (node.intelamt.state == 2)) { str += '<i>' + "Aktivoitu" + '</i>'; }
                    else if ((node.intelamt.ver == null) || (node.intelamt.state == null)) { str += '<i>' + "Tuntematon versio ja tila" + '</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 on aktivoitu asiakasohjaustilassa" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { str += ' <span title="' + "Intel&reg; AMT on aktivoitu Järjestelmänvalvojan ohjaustilassa" + '">' + "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 on määritetty TLS-verkon suojauksella" + '">' + "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="' + "Muokkaa Intel&reg; AMT -tilitietoja" + '" onclick=editDeviceAmtSettings("' + node._id + '")>' + "Ei tunnuksia" + '</i>';
                                editUserCredentialsIcon = true;
                            } else {
                                str += ', <i style=color:#FF0000>' + "Ei tunnuksia" + '</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 = "Virheelliset kirjautumistiedot"; }
                            if ((node.intelamt.warn & 8) != 0) { warn = "Kokeillaan kirjautumistietoja"; }
                            if (warn != null) {
                                if ((meshrights & 4) != 0) {
                                    str += ', <i style=color:#FF0000;cursor:pointer title="' + "Muokkaa Intel&reg; AMT -tilitietoja" + '" 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="' + "Muokkaa Intel&reg; AMT -tilitietoja" + '" style=cursor:pointer onclick=editDeviceAmtSettings("' + node._id + '")>';
                        }
                    }

                    var meName = '<span title="' + "Intel &reg; hallittavuusmoottori" + '">' + "Intel&reg; ME" + '<span>';
                    if (typeof node.intelamt.sku == 'number') {
                        if ((node.intelamt.sku & 8) != 0) { meName = '<span title="' + "Intel&reg; Active Management-tekniikka" + '">' + "Intel&reg; AMT" + '<span>'; }
                        else if ((node.intelamt.sku & 16) != 0) { meName = '<span title="' + "Intel&reg; standardinhallinta" + '">' + "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("Agentin lappu", 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 -tunniste", 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("Käyttöjärjestelmä", 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>' + "HUONO" + '</span>'); } }
                    if (node.wsc.autoUpdate != null) { if (node.wsc.autoUpdate == 'OK') { y.push("Päivittää" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Päivittää" + ' - <span style=color:red>' + "HUONO" + '</span>'); } }
                    if (node.wsc.firewall != null) { if (node.wsc.firewall == 'OK') { y.push("Palomuuri" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Palomuuri" + ' - <span style=color:red>' + "HUONO" + '</span>'); } }
                    x += addDeviceAttribute("Windowsin suojaus", y.join(', '));
                }

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

                // Linux Security Central
                if (node.lsc) {
                    var y = [];
                    if (node.lsc.antiVirus != null) { if (node.lsc.antiVirus == 'OK') { y.push("AV" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("AV" + ' - <span style=color:red>' + "HUONO" + '</span>'); } }
                    if (node.lsc.firewall != null) { if (node.lsc.firewall == 'OK') { y.push("Palomuuri" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Palomuuri" + ' - <span style=color:red>' + "HUONO" + '</span>'); } }
                    if (y.length > 0) x += addDeviceAttribute("Linuxin suojaus", 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>' + "Poistettu käytöstä" + '</span>'; }
                            if (node.av[i].updated !== true) { avx += ' - <span style=color:red>' + "Vanhentunut" + '</span>'; }
                            if ((node.av[i].enabled == true) && (node.av[i].updated == true)) { avx += ' - <span style=color:green>' + "OK" + '</span>'; }
                            y.push(avx);
                        }
                    }
                    x += addDeviceAttribute("Antivirus", y.join('<br />'));
                }

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

                // Windows Idle Time
                if (node.idletime && node.idletime != -1) { x += addDeviceAttribute("Lepotila", 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("Työpöydän kehote+työkalurivi"); } else if (consent & 0x0040) { meshFeatures.push("Työpöydän työkalurivi"); } else if (consent & 0x0008) { meshFeatures.push("Työpöydän kehote"); } else { if (consent & 0x0001) { meshFeatures.push("Työpöytä ilmoitus"); } }
                    if (consent & 0x0010) { meshFeatures.push("Terminaalikehote"); } else { if (consent & 0x0002) { meshFeatures.push("Ilmoita terminaalista"); } }
                    if (consent & 0x0020) { meshFeatures.push("Tiedostot kysy"); } else { if (consent & 0x0004) { meshFeatures.push("Tiedostot Ilmoita"); } }
                    if (consent == 7) { meshFeatures = ["Ilmoita aina"]; }
                    if ((consent & 56) == 56) { meshFeatures = ["Kysy Aina"]; }

                    meshFeatures = meshFeatures.join(', ');
                    if (meshFeatures == '') { meshFeatures = '<i>' + "Ei mitään" + '</i>'; }
                    x += addDeviceAttribute("Käyttäjän suostumus", 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("Yhdistä"); }
                    if (devNotify & 4) { devNotifyStr.push("Katkaise yhteys"); }
                    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("Email ({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("Messaging ({0})", xx)); }
                    }
                }
                devNotifyStr = devNotifyStr.join(', ');
                if (devNotifyStr == '') { devNotifyStr = '<i>' + "Ei mitään" + '</i>'; }
                x += addDeviceAttribute("Ilmoitukset", 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="' + "Mesh agentti on yhdistetty ja käyttövalmis." + '">' + "Mesh Agentti" + '</span>');
                    if ((node.conn & 2) != 0) cstate.push('<span title="' + "Intel&reg; AMT CIRA yhdistetty ja valmis käytettäväksi." + '">' + "Intel&reg; AMT CIRA" + '</span>');
                    else if ((node.conn & 4) != 0) cstate.push('<span title="' + "Intel&reg; AMT on reititettävä ja käyttövalmis." + '">' + "Intel&reg; AMT" + '</span>');
                    if ((node.conn & 8) != 0) cstate.push('<span title="' + "Verkkoagentti on tavoitettavissa käyttämällä toista ainetta releenä." + '">' + "Mesh-rele" + '</span>');
                    if ((node.conn & 16) != 0) { cstate.push('<span title="' + "MQTT-yhteys laitteeseen on aktiivinen." + '">' + "MQTT" + '</span>'); }
                    x += addDeviceAttribute("Liitettävyys", cstate.join(', '));
                }

                // Node tags
                var groupingTags = '<i>' + "Ei mitään" + '</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("Tunnisteet", '<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("Tunnisteet", '<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-käyttäjä+pass" : ((node.ssh == 2) ? "SSH-käyttäjä+avain+pass" : "SSH-käyttäjä+avain")) + ' <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-käyttäjä+pass" : ((node.ssh == 2) ? "SSH-käyttäjä+avain+pass" : "SSH-käyttäjä+avain"))); }
                        if (node.rdp != null) { y.push("RDP"); }
                    }
                    x += addDeviceAttribute("Valtakirjat", 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="' + "Laiteryhmät, joille tämä laite on rele" + '">' + "Rele varten" + '</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="' + "Toiminnot" + '" title="' + "Virranhallintatoimintojen suorittaminen laitteessa" + '" onclick=deviceActionFunction() />'; }
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Muistiinpanot" + '" title="' + "Näytä tämän laitteen huomautukset" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Kirjaa tapahtuma" + '" title="' + "Kirjoita tapahtuma tälle laitteelle" + '" 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="' + "Run" + '" title="' + "Suorita komentoja tässä laittessa." + '" 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="' + "Viesti" + '" title="' + "Näytä tekstiviesti etälaitteessa" + '" onclick=deviceMessageFunction() />'; }
                    //if ((connectivity & 1) && (meshrights & 8) && (node.agent.id < 5)) { x += '<input type=button value=Toast title="' + "Display a text message of the remote device" + '" onclick=deviceToastFunction() />';  }
                    if ((meshrights & 8) && (connectivity & 1) && ((meshrights & 16384) != 0) || ((node.pmt == 1) && ((features2 & 2) != 0))) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Chat" + '" title="' + "Avaa chat-ikkuna tälle tietokoneelle" + '" 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="' + "Jaa" + '" title="' + "Luo linkki tämän laitteen jakamiseksi vieraan kanssa" + '" 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="' + "Sammuttaa" + '" title="' + "Sammuttaa" + '" 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="' + "Kiihottua" + '" title="' + "Kiihottua" + '" onclick=setIpPduState(1) />'; }
                        }
                    } else {
                        if (meshrights & 8) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Etähallinta" + '" title="' + "Etähallinta" + '" 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="' + "Siirrä tämä laite toiseen laiteryhmään" + '">' + "Vaihda Ryhmää" + '</a>';
                }
                if ((meshrights & 0x8000) != 0) { // MESHRIGHT_UNINSTALL
                    x += '&nbsp;<a href=# onclick=p10showDeleteNodeDialog("' + node._id + '") title="' + "Poista tämä laite" + '">' + "Poista laite" + '</a>';
                }
                x += '</div><div class="p10html3left">';
                if ((node.agent) && (node.mtype != 3) && ((meshrights & 1048576) != 0)) x += '<a href=# onclick=p10showNodeNetInfoDialog("' + node._id + '") title="' + "Näytä laitteen verkkoliittymän tiedot" + '">' + "Liitennät" + '</a>&nbsp;';
                if ((features & 0x00008000) && (xxmap != null)) x += '<a href=# onclick=p10showNodeLocationDialog("' + node._id + '") title="' + "Näytä laitteen sijaintitiedot" + '">' + "Sijainti" + '</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="' + "Liikennereititin, jota käytetään yhteyden muodostamiseen laitteeseen tämän palvelimen kautta" + '.">' + "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="' + "Avaa XTerm-pääte" + '">' + "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="' + "Edellyttää MeshCentral-reitittimen asennusta" + '.">' + "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="' + "Edellyttää MeshCentral-reitittimen asennusta." + '">' + "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="' + "Edellyttää MeshCentral-reitittimen asennusta." + '">' + "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="' + "Edellyttää MeshCentral-reitittimen asennusta." + '">' + 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="' + "Käynnistä verkkopohjainen VNC-istunto tälle laitteelle" + '.">' + "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="' + "Käynnistä web-pohjainen RDP-istunto tähän laitteeseen" + '.">' + "Verkko-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="' + "Käynnistä verkkopohjainen SSH-istunto tälle laitteelle" + '.">' + "Web-SSH" + '</a>&nbsp;';
                    }

                    // MQTT options
                    if ((meshrights == 0xFFFFFFFF) && (features & 0x00400000)) { x += '<a href=# onclick=p10showMqttLoginDialog("' + node._id + '") title="' + "Hanki MQTT-kirjautumistiedot tälle laitteelle." + '">' + "MQTT kirjautuminen" + '</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="' + "Agentti toimii etälaitteella, jolla on rajoitetut oikeudet." + '">' + "Rajoitettu" + '</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="' + "Kytkimen portti kytketty" + '">' + "Kytkimen portti kytketty" + '</span>' + agentPrivilages;
                        } else {
                            powerstate += '<span style=font-size:12px title="' + "IP-KVM-portti kytketty" + '">' + "IP-KVM-portti kytketty" + '</span>' + agentPrivilages;
                        }
                    } else {
                        powerstate += '<span style=font-size:12px title="' + "Agentti yhdistetty" + '">' + "Agentti yhdistetty" + '</span>' + agentPrivilages;
                    }
                }
                if ((connectivity & 2) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT yhdistetty" + '">' + "Intel&reg; AMT yhdistetty" + '</span>'; }
                else if ((connectivity & 4) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT havaittu" + '">' + "Intel&reg; AMT havaittu" + '</span>'; }
                if ((connectivity & 16) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "MQTT yhdistetty" + '">' + "MQTT kanava yhdistetty" + '</span>'; }
                if ((powerstate == '') && node.lastconnect) { powerstate = '<span style=font-size:12px>' + "Viimeksi nähty:" + '<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> ' + "Lisää Käyttäjä" + '</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> ' + "Lisää Käyttäjä Ryhmä" + '</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>' + "Käyttäjäluvat" + '</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="' + "Poista käyttäjäryhmän oikeudet tähän laiteryhmään" + '" 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="' + "Posta käyttäjältä oikeudet tähän laiteryhmään" + '" 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 ? "Käyttäjäryhmä" : "Käyttäjä") + '" 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>' + "Ei käyttäjiä, joilla on erityiset laitteen käyttöoikeudet" + '</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>' + "Aktiivinen laitteen jakaminen" + '</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="' + "Laitteen jakolinkki" + '" 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="' + "Poista laitteen jakaminen" + '" class="fa-fw fa-solid fa-trash text-danger"></i>';
                        var type = ''; if (dshare.p <= 7) { type = ['', "Pääte", "Työpöytä", "Työpöytä + pääte", "Tiedostot", "Pääte + tiedostot", "Työpöytä + tiedostot", "Työpöytä + pääte + tiedostot"][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} minuutin ajan", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            } else {
                                details = format("{0}, {1} {2} minuutin ajan", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            }
                        }
                        if (dshare.recurring == 1) { details += ", Toistuu päivittäin"; }
                        if (dshare.recurring == 2) { details += ", Toistuu viikoittain"; }
                        if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Näytä vain työpöytä"; }
                        if (dshare.consent != null) {
                            if (dshare.consent == 0) { details += ", Ei suostumusta"; } else {
                                if ((dshare.consent & 0x0038) != 0) { details += ", Pyydä suostumusta"; }
                                if ((dshare.consent & 0x0040) != 0) { details += ", Työkalupalkki"; }
                            }
                        }
                        var guestName = EscapeHtml(dshare.guestName);
                        if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Agentin omavastuu" + '</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', "Virtakäyttö", 'Perform power off?');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: 2 }); });
            } else {
                // Turn on
                setModalContent('xxAddAgent', "Virtakäyttö", '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">' + "Web-sivun ilmoitukset" + '</div>';
            x += '<div><label><input id=p20notifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Laiteyhteydet" + '</label></div>';
            x += '<div><label><input id=p20notifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Laitteen irtikytkennät" + '</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 työpöytä ja sarjaliikenne ilmoitukset" + '</label></div>'; }
            if (fx & 1) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "sähköposti-ilmoitukset" + '</div>';
                x += '<div><label><input id=p20enotifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Laiteyhteydet" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Laitteen irtikytkennät" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceHelp type=checkbox class="form-check-input me-2" />' + "Help requests" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Messaging Notifications" + '</div>';
                x += '<div><label><input id=p20emsgDeviceConnect type=checkbox class="form-check-input me-2" />' + "Laiteyhteydet" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Laitteen irtikytkennät" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceHelp type=checkbox class="form-check-input me-2" />' + "Help requests" + '</label></div>';
            }
            setModalContent('xxAddAgent', "Ilmoitusasetukset", 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 "Laitteen täydet oikeudet"; }
            var str = [];
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Ei tuloa");
                if (rights & 512) str1.push("Ei päätettä");
                if (rights & 1024) str1.push("Ei tiedostoja");
                if (rights & 2048) str1.push("Ei AMT");
                if (rights & 4096) str1.push("Rajoitettu syöttö");
                if (rights & 65536) str1.push("Ei työpöytää");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Vieraiden jakaminen");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Hallinta"); }
            }
            if (rights & 16) str.push("Konsoli");
            if (rights & 32) str.push("Palvelimen Tiedostot");
            if (rights & 64) str.push("Herätä");
            if (rights & 128) str.push("Muistiinpanot");
            if (rights & 8192) str.push("Rajoita tapahtumia");
            if (rights & 16384) str.push("Chat");
            if (rights & 32768) str.push("Asennuksen poistaminen");
            if (rights & 131072) str.push("Komennot");
            if (rights & 262144) str.push("Nollaa / Pois");
            if (rights & 524288) str.push("Jakaminen");
            if (rights & 1048576) str.push("Tarkemmat tiedot");
            if (rights & 2097152) str.push("Rele");
            if (str.length == 0) return "Ei oikeuksia";
            return str.join(', ');
        }

        function makeDeviceGroupRightsString(rights) {
            if (rights == 0xFFFFFFFF) { return "Täydet oikeudet"; }
            var str = [];
            if (rights & 1) str.push("Muokkaa ryhmää");
            if (rights & 2) str.push("Hallitse käyttäjiä");
            if (rights & 4) str.push("Hallitse laitteita");
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Ei tuloa");
                if (rights & 512) str1.push("Ei päätettä");
                if (rights & 1024) str1.push("Ei tiedostoja");
                if (rights & 2048) str1.push("Ei AMT");
                if (rights & 4096) str1.push("Rajoitettu syöttö");
                if (rights & 65536) str1.push("Ei työpöytää");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Vieraiden jakaminen");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Hallinta"); }
            }
            if (rights & 16) str.push("Konsoli");
            if (rights & 32) str.push("Palvelimen Tiedostot");
            if (rights & 64) str.push("Herätä");
            if (rights & 128) str.push("Muistiinpanot");
            if (rights & 8192) str.push("Rajoita tapahtumia");
            if (rights & 16384) str.push("Chat");
            if (rights & 32768) str.push("Asennuksen poistaminen");
            if (rights & 131072) str.push("Komennot");
            if (rights & 262144) str.push("Nollaa / Pois");
            if (rights & 524288) str.push("Jakaminen");
            if (rights & 1048576) str.push("Tarkemmat tiedot");
            if (rights & 2097152) str.push("Rele");
            if (str.length == 0) return "Ei oikeuksia";
            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', "Lisää laitetapahtuma", '<textarea id=d2devEvent style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea><span style=font-size:10px>' + "Tämä lisää merkinnän tämän laitteen tapahtumalokiin." + '<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>' + "Laiteryhmän muistiinpanoja voivat tarkastella ja muuttaa muut laiteryhmän järjestelmänvalvojat." + '<span>'; }
            if (showNotesPanel === 'true') { x += ' <span style=font-size:10px><a target=_blank href=\'https://www.markdownguide.org/cheat-sheet/\'>' + "Markdown-syntaksi tuettu" + '</a></span>'; }
            setModalContent('xxAddAgent', "Muistiinpanot", 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', "Avaa sivu laitteella", '<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>' + "Näytä viestiruutu etälaitteessa." + '</div>';
            x += '<textarea id=d2devMessage style=width:100%;height:80px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px></textarea>';
            x += '<select style=width:100% id=d2devTimeout>';
            x += '<option value=2 selected>' + "Show for 2 Minutes (Default)" + '</option>';
            x += '<option value=10>' + "Show for 10 minutes" + '</option>';
            x += '<option value=30>' + "Show for 30 minutes" + '</option>';
            x += '<option value=60>' + "Show for 60 minutes" + '</option>';
            x += '<option value=0>' + "Näytä viesti, kunnes käyttäjä hylkäsi sen" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Laiteviesti", 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', "Etätulon lukitus", (value == 1) ? "Lukitaanko etäkäyttäjän hiiri ja näppäimistö?" : "Avaa etäkäyttäjän hiiren ja näppäimistön lukitus?");
            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>' + "Näyttönäppäimistö"
                            + ' <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',         "Poistu koko näytöltä",                          'deskToggleFull();deskCloseMobileActions()',                       'text-danger') +
                monitorRow +
                row(cls, (_mobileZoomMode==='native' ? 'fa-compress-arrows-alt' : 'fa-expand-arrows-alt'),
                    (_mobileZoomMode==='native' ? "Sovita kokoon" : "Alkuperäinen koko (vieritettävä)"),  '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',          "Näyttönäppäimistö" + ' <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 ? "Poista kosketuslevytila käytöstä" : "Ota kosketuslevytila käyttöön"),  'deskToggleTrackpad();deskCloseMobileActions()') +
                row(cls, 'fa-hand-pointer',      (swap ? "Poista hiiren oikean painikkeen tila käytöstä" : "Ota hiiren oikean painikkeen tila käyttöön"),   'deskToggleRightClickMode();deskCloseMobileActions()') +
                row(cls, 'fa-font',              "Kirjoita teksti",                                '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',           "Pikanäppäimet",                       'deskCustomizeKeys();deskCloseMobileActions()') +
                row(cls, 'fa-paste',             "Leikepöytä",                               'showDeskClip();deskCloseMobileActions()') +
                row(cls, 'fa-clipboard',         "Lataa leikepöytä",                         'deskClipboardOutFunction();deskCloseMobileActions()') +
                row(cls, 'fa-rotate-right',      "Kierrä oikealle",                             'drotate(1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate-left',       "Käänny vasemmalle",                              'drotate(-1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate',            "Päivitä työpöytä",                          'deskRefreshFunction();deskCloseMobileActions()') +
                row(cls, 'fa-gear',              "Työpöydän asetukset",                         'showDesktopSettings();deskCloseMobileActions()') +
                row(end, 'fa-plug-circle-xmark', "Katkaise yhteys",                               '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>' + "Toast-ilmoitus" + '</option><option value=1>' + "Viestilaatikko" + '</option><option value=3>' + "Alert Box" + '</option></select>';
            x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Otsikko" + '" style=width:100%;box-sizing:border-box;margin-bottom:4px class=form-control/>';
            x += '<textarea id=d2notifyMsg style=background-color:#fcf3cf;width:100%;height:140px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px class=form-control></textarea>';
            x += '<select style=width:100% id=d2notifyTimeout class=form-select>';
            x += '<option disabled value="">Only Applicable to Message Box Notifications</option>';
            x += '<option value=2 selected>' + "Show for 2 Minutes (Default)" + '</option>';
            x += '<option value=10>' + "Show for 10 minutes" + '</option>';
            x += '<option value=30>' + "Show for 30 minutes" + '</option>';
            x += '<option value=60>' + "Show for 60 minutes" + '</option>';
            x += '<option value=0>' + "Näytä viesti, kunnes käyttäjä hylkäsi sen" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Laiteilmoitus", 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', "Lukitse työpöytä", "Lukitaanko käyttäjän työpöytä?");
                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 = "Luo linkin, jonka avulla vieras, jolla ei ole tiliä, voi ohjata tätä laitetta rajoitetun ajan." + '<br /><br />';
            x += addHtmlFormFloating("Vieraan nimi", '<input id=d2inviteName class="form-control" maxlength=128 type=text onkeyup=showShareDeviceValidate() />');
            var deskFull = '<option value=2>' + "Työpöytä" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x100) != 0)) { deskFull = ''; }
            var fullTerm = '<option value=1>' + "Pääte" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x200) != 0)) { fullTerm = ''; }
            var fullFiles = '<option value=4>' + "Tiedostot" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x400) != 0)) { fullFiles = ''; }
            var deskFiles = '<option value=5>' + "Työpöytä + tiedostot" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x500) != 0)) { deskFiles = ''; }
            var termFiles = '<option value=6>' + "Pääte + tiedostot" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x600) != 0)) { termFiles = ''; }
            var allFeatures = '<option value=7>' + "Työpöytä + pääte + tiedostot" + '</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>' + "Työpöytä, vain näkymä" + '</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("Tyyppi", '<select id=d2shareType class="form-select" onchange=showShareDeviceValidate()>' + y + '</select>');
            var options = { 1: "1 minuutti", 5: "5 minuuttia", 10: "10 minuuttia", 15: "15 minuuttia", 30: "30 minuuttia", 45: "45 minuuttia", 60: "60 minuuttia", 120: "2 tuntia", 240: "4 tuntia", 480: "8 tuntia", 720: "12 tuntia", 960: "16 tuntia", 1440: "24 tuntia", 2880: "2 päivää", 5760: "4 päivää", 0: "Rajoittamaton" }
            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("Voimassaolo", '<select id=d2timeRange class="form-select" onchange=showShareDeviceValidate()><option value=0>' + "Alkaen nyt" + '</option><option value=1>' + "Aikahaarukka" + '</option><option value=2>' + "Toistuva päivittäin" + '</option><option value=3>' + "Toistuu viikoittain" + '</option></select>');
            x += '<div id=d2modenow>';
            x += addHtmlFormFloating("Vanhentumisaika", '<select id=d2inviteExpire class="form-select">' + y + '</select>');
            x += '</div><div id=d2moderange style=display:none>';
            x += addHtmlFormFloating("Aika -alue alkaa", '<input id=d2timeRangeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Valitse päivämäärä ja aika..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Aikaväli", '<input id=d2timeRangeEndSelector class="flatpickr form-select" type="text" placeholder="' + "Valitse päivämäärä ja aika..." + '" data-id="altinput">');
            x += '</div><div id=d2moderecurring style=display:none>';
            x += addHtmlFormFloating("Käynnistys aika", '<input id=d2timeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Valitse päivämäärä ja aika..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Kesto", '<select id=d2inviteDuration class="form-select">' + z + '</select>');
            x += '</div>';
            if (currentNode.agent.caps & 1) { x += '<div id=d2userConsentSelector>' + addHtmlFormFloating("Käyttäjän suostumus", '<select id=d2userConsent class="form-select"><option value=0>' + "Ilmoita vain" + '<option value=1>' + "Kysy suostumusta" + '</option><option value=2>' + "No Consent" + '</option></select>') + '</div>'; }
            x += '<div id=d2httpPortSelector>' + addHtmlFormFloating("Port", '<input id=d2httpPort class="form-control" value=80 onkeyup=showShareDeviceValidate()></input>') + '</div>';
            x += '<div id=d2httpsPortSelector>' + addHtmlFormFloating("Port", '<input id=d2httpsPort class="form-control" value=443 onkeyup=showShareDeviceValidate()></input>') + '</div>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Jaa laite", 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 = "Valitse suoritettava toiminto tällä laitteella." + '<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>' + "Salama" + '</option>';
                    y += '<option value=401>' + "Värinä" + '</option>';
                    z += '<div id=d2devicetimediv>' + addHtmlFormFloating("Aika", '<select id=d2devicetime class=form-select><option value=1000>' + "1 sekunti" + '</option><option value=5000>' + "5 sekuntia" + '</option><option value=10000>' + "10 sekuntia" + '</option></select>') + '</div>';
                }
            } else {
                if ((rights & 64) != 0) { count++; y += '<option value=100>' + "Herätä" + '</option>'; } // Wake-up permission
                if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Suorita Komentoja" + '</option>'; } // Remote command permission
                if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Nukkua" + '</option><option value=3>' + "Nollaa" + '</option><option value=2>' + "Sammuta" + '</option>'; }
                if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Lähetä MQTT Viesti" + '</option>'; }
                if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
                    count++;
                    y += '<option value=310>' + "Intel&reg; AMT Reset" + '</option>';
                    y += '<option value=308>' + "Intel&reg; AMT Katkaise virta" + '</option>';
                    if ((xxcurrentView == 11) || (xxcurrentView == 12)) { // Only show these options on terminal or desktop tabs
                        y += '<option value=312>' + "Intel&reg; AMT Reset to BIOS" + '</option>';
                        y += '<option value=311>' + "Intel&reg; AMT Power on to BIOS" + '</option>';
                        y += '<option value=315>' + "Intel&reg; AMT Power on to PXE" + '</option>';
                        y += '<option value=316>' + "Intel&reg; AMT Reset to PXE" + '</option>';
                    }
                }
                if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
                    count++;
                    y += '<option value=302>' + "Intel&reg; AMT Virta päällä" + '</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 yhden napsautuksen palautus" + '</option>'; } // CIRA (2) or AMT (4) connected
                if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Agentin asennuksen poistaminen" + '</option>'; }
            }
            y += '</select>';
            x += addHtmlFormFloating("Operaatio", y);
            if (count == 0) { x = "Tällä laitteella ei ole tällä hetkellä käytettävissä toimintoja."; }
            xxdialogButtons = (count == 0) ? 2 : 3; xxdialogTag = x + z;
            setModalContent('xxAddAgent', "Laitetoiminto", 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 = "Suorita komentoja valituilla laitteilla." + '<br />';
                    if (wintype == true) {
                        x += '<select id=d2cmdtype style=width:100%;margin-bottom:4px;margin-top:4px> class=form-select>';
                        x += '<option value=1>' + "Windowsin komentohehote" + '</option><option value=2>' + "Windows PowerShell" + '</option>';
                        if (linuxtype == true) { x += '<option value=3>' + "Linux/BSD/macOS komentokehote" + '</option>'; }
                        x += '</select>';
                    }
                    x += '<select id=d2cmduser style=width:100%;margin-bottom:4px><option value=0>' + "Suorita agenttina" + '</option><option value=1>' + "Suorita käyttäjänä, agenttina, jos ei käyttäjää" + '</option><option value=2>' + "On suoritettava käyttäjänä" + '</option></select>';
                    x += '<textarea id=d2runcmd style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea>';
                    setModalContent('xxAddAgent', "Suorita Komentoja", 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 yhden napsautuksen palautus", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deviceActionOneClickRecovery());
                d3init();
            } else if (op == 302) { // Intel AMT power on
                setModalContent('xxAddAgent', "Intel&reg; AMT -käyttö", "Otetaanko Intel&reg; AMT käyttöön?");
                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 -käyttö", "Perform Intel&reg; AMT power off?<br><br><b>NOTE: If there is an active AMT session, then power off command will be rejected, so you must disconnect from the AMT session first!</b>");
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: parseInt(op) }); });
            } else if (op == 310) { // Intel AMT reset
                setModalContent('xxAddAgent', "Intel&reg; AMT -käyttö", "Suoritetaanko Intel&reg; AMT -nollaus?");
                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 -käyttö", "Perform Intel&reg; AMT power on to 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 -käyttö", "Perform Intel&reg; AMT reset to 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} välillä {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>' + "Päivä" + '</th><th scope=col>' + "7 päivän virtatila" + '<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="' + "Lataa virtatapahtumat" + '" 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("Käyttäjätunnus", '<input id=dp10username class="form-control" maxlength=32 autocomplete=nope placeholder="admin" onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            x += addHtmlFormFloating("Salasana", '<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("Turvallisuus", '<select id=dp10tls class="form-select"><option value=0>' + "Ei TLS-suojausta" + '</option><option value=1>' + "TLS-suojaus vaaditaan" + '</option></select>'); }
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { buttons = 7; }
            setModalContent('xxAddAgent', "Muokkaa Intel&reg; AMT -tilitietoja", 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("Aihe", '<input id=dp2topic class="form-control" maxlength=64 onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1) />');
            x += addHtmlFormFloating("Viesti", '<textarea id=dp2msg class="form-control" maxlength=4096 style=width:100%;height:150px;resize:none onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1)></textarea>');
            setModalContent('xxAddAgent', "Lähetä MQTT viesti", 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("Haluatko varmasti poistaa valitut {0} agenttia?", nodeids.length); } else { x = "Haluatko varmasti poistaa valitun agentin?"; }
            x += '<br /><br />';
            if (nodeids.length > 1) { x += "Tämä ei poista laitteita palvelimelta, mutta laitteet eivät enää voi muodostaa yhteyttä palvelimeen. Kaikki laitteiden etäkäyttö menetetään. Laitteet on kytkettävä, jotta tämä komento toimisi."; } else { x += "Tätä laitetta ei poisteta palvelimelta, mutta laite ei voi enää muodostaa yhteyttä palvelimeen. Kaikki laitteen etäkäyttö menetetään. Laitteen on oltava kytketty, jotta tämä komento toimisi."; }
            x += '<br /><br /><label style=color:red><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Varmista" + '</label>';
            setModalContent('xxAddAgent', "Agentin asennuksen poistaminen", 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) ? ("Valitse uusi ryhmä tälle laitteelle" + '<br /><br />') : ("Valitse uusi ryhmä valituille laitteille" + '<br /><br />');
                x += addHtmlFormFloating("Uusi laiteryhmä", y);
                setModalContent('xxAddAgent', "Vaihda Ryhmää", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showChangeGroupDialogEx(3, nodeids));
            } else {
                setModalContent('xxAddAgent', "Vaihda Ryhmää", '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("Haluatko varmasti poistaa solmun {0}?", EscapeHtml(currentNode.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Varmista" + '</label>';
            xxdialogButtons = 3; xxdialogTag = nodeid;
            setModalContent('xxAddAgent', "Poista solmu", 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', "Laitteen sijainti", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            return false;
        }

        // Show network interfaces
        function p10showNodeNetInfoDialog() {
            if (xxdialogMode) return false;
            xxdialogButtons = 1; xxdialogTag = 'if' + currentNode._id;
            setModalContent('xxAddAgent', "Verkkoliitännät", '<div id=d2netinfo>' + "Ladataan..." + '</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 Reititin on Windows-työkalu TCP-porttien tunneloimiseen. Voit esimerkiksi yhdistää etälaitteesi RDP:n tämän palvelimen kautta." + '</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">' + "Suorita MeshCentral Reititin ja napsauta \"asenna\", jotta se voidaan käynnistää selaimesta." + '</div>';
            x += '<br /><a style=cursor:pointer target="mcrouterframe" rel="noreferrer noopener" href="' + url + '"><input type=button style=width:100%;cursor:pointer value="' + "Käynnistä MeshCentral Reititin" + '" class="btn btn-primary btn-block" /></a>';
            x += '<iframe class="d-none" name="mcrouterframe"></iframe>';
            setModalContent('xxAddAgent', "MeshCentral Reititin", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p20showAddMeshUserDialogEx(1, 'fileDownload');
            });
        }

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

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

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

            var x = '';
            if (mode == 0) { x += '<div>' + "MeshCmd on komentorivityökalu, joka suorittaa paljon erilaisia ​​toimintoja. Toimintatiedosto voidaan valinnaisesti ladata ja muokata palvelintietojen ja tunnistetietojen tarjoamiseksi." + '<br /><br />'; }
            if (mode == 1) { x += '<div>' + "Lataa \"meshcmd\", toimintotiedoston avulla liikenteen ohjaamiseksi palvelimelta tähän laitteeseen. Muista muokata meshaction.txt ja lisätä tilisi salasana tai tehdä tarvittavat muutokset." + '<br /><br />'; }
            x += addHtmlFormFloating("Käyttöjärjestelmä", 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("Toimintotiedosto", '<a onclick=downloadFile("meshagents?meshaction=generic' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary>' + "MeshAction (.txt)" + '</a>'); }
            if (mode == 1) { x += addHtmlValue("Toimintotiedosto", '<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', "Lataa 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 executable)"; }
            if (os == 4) { osn = "MeshCmd (Win x86-64 executable)"; }
            if (os == 43) { osn = "MeshCmd (Win ARM-64 executable)"; }
            if (os == 5) { osn = "MeshCmd (Linux x86, 32bit)"; }
            if (os == 6) { osn = "MeshCmd (Linux x86, 64bit)"; }
            if (os == 16) { osn = "MeshCmd (macOS, x86-64-bittinen)"; }
            if (os == 29) { osn = "MeshCmd (macOS, ARM-64-bittinen)"; }
            if (os == 25) { osn = "MeshCmd (Linux ARM, 32bit)"; }
            if (os == 26) { osn = "MeshCmd (Linux ARM, 64bit)"; }
            QH('meshcmddownloadid', osn);
            Q('meshcmddownloadid').onclick = function () { downloadFile('meshagents?meshcmd=' + os + (urlargs.key ? ('&key=' + urlargs.key) : '')); }
        }

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

            var x = '<div style=text-align:center><br />';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i1 onclick=p10setIcon(1) onkeypress="if (event.key==\'Enter\') p10setIcon(1)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i2 onclick=p10setIcon(2) onkeypress="if (event.key==\'Enter\') p10setIcon(2)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i3 onclick=p10setIcon(3) onkeypress="if (event.key==\'Enter\') p10setIcon(3)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i4 onclick=p10setIcon(4) onkeypress="if (event.key==\'Enter\') p10setIcon(4)"></div>';
            x += '<br />';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i5 onclick=p10setIcon(5) onkeypress="if (event.key==\'Enter\') p10setIcon(5)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i6 onclick=p10setIcon(6) onkeypress="if (event.key==\'Enter\') p10setIcon(6)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i7 onclick=p10setIcon(7) onkeypress="if (event.key==\'Enter\') p10setIcon(7)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i8 onclick=p10setIcon(8) onkeypress="if (event.key==\'Enter\') p10setIcon(8)"></div>';
            x += '<br /><br /></div>';
            setModalContent('xxAddAgent', "Kuvakkeen valinta", 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', "Muokkaa laitetta", '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', "Muokkaa laitetta", '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 = ["Laitteen Nimi", "Isäntänimi", "Kuvaus", "Tunnisteet"];
        var showEditNodeValueDialog_modes2 = ['name', 'host', 'desc', 'tags'];
        var showEditNodeValueDialog_modes3 = ['', '', '', "Tag1, Tag2, Tag3"];
        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', "Muokkaa laitetta", x);
                if (Array.isArray(v)) { v = v.join(', '); }
                Q('dp10devicevalue').value = v;
            }
            p10editdevicevalueValidate();
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { showEditNodeValueDialogEx(3, mode); });
            Q('dp10devicevalue').focus();
        }

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

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

        //
        // DESKTOP
        //

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

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

            updateMetadata(desktop, 'deskmetadata');
        }

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

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

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

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

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

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

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

        // Used to translate incoming agent console messages
        var agentConsoleMessages = ['', "Odotetaan, että käyttäjä myöntää käyttöoikeuden...", "Evätty", "Etäpääteistunnon aloittaminen epäonnistui, {0} ({1})", "Aikakatkaisu", "Vastaanotettu virheellisiä verkkotietoja", "Näytön kaappaus ei onnistu", "Protokollaneuvottelu epäonnistui ({0})", "NLA:ta ei tueta", "Palvelimen vaatima SSL", "Palvelin ei salli SSL:ää", "SSL-varmenne ei ole palvelimella", "Epäjohdonmukaiset liput", "Palvelimen vaatima hybridi", "Palvelimen vaatima SSL käyttäjän todennus"];
        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("Tietokanava", {}); // { 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("Valtakirjat", '<select id=d2mode class="form-select" onchange=askRdpCredentialsValidate()><option value=1>' + "Käytä palvelimen kirjautumistietoja" + '</option><option value=2>' + "Käytä uusia kirjautumistietoja" + '</option></select>');
                x += '<div id=d2cred style=display:none>';
            } else {
                x += '<div id=d2cred>';
            }
            x += addHtmlFormFloating("Verkkotunnus", '<input type=text id=d2domain class="form-control" maxlength=128 />');
            x += addHtmlFormFloating("Käyttäjätunnus", '<input type=text id=d2user class="form-control" onKeyUp=askRdpCredentialsValidate() maxlength=128 />');
            x += addHtmlFormFloating("Salasana", '<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>' + "Muista valtakirjat" + '</label>'); }
            x += '</div>';
            x += MoreStart();
            x += addHtmlFormFloating("Alt Shell", '<input type=text id=d2altshell class="form-control" maxlength=2048 />');
            x += addHtmlFormFloating("Työskentely Dir", '<input type=text id=d2workdir class="form-control" maxlength=2048 />');
            x += MoreEnd();
            setModalContent('xxAddAgent', "RDP:n valtuustiedot", 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} katselee", 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("Käynnistys aika", 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 yhteys" : format("{0} yhteyttä", conn.metadata.users[i]);
                        x += addHtmlValue2(getUserName(i), val);
                    }
                }
                xxdialogTag = 'sessionMetadata' + cid;
                setModalContent('xxAddAgent', "Istunnon tiedot", 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 = "Kaikki keskittyvät";
                    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', "Etätyöpöydän asetukset", 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 = "Kaikki keskittyvät"; }
                    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 = ["Kaikki keskittyvät", "Pieni tarkennus", "Suuri tarkennus"][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: "Välilehti", 13: "Tulla sisään", 27: "Paeta", 32: "Space", 44: "Tulosta näyttö", 45: "Lisää", 46: "Del", 36: "Koti", 35: "Loppu", 33: "Sivu ylös", 34: "Sivu alas", 37: "Vasen", 38: "Ylös", 39: "Aivan", 40: "Alas", 0: "Ei mitään" }

        function keyShortcutTotext(n) {
            var x = [];
            if (n & 0x010000) { x.push("Siirtää"); }
            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', "Pikanäppäimet", 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("Nimi", '<input type=text id=d2shortcutname class="form-control" maxlength=32 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating("Arvo", '<input type=text id=d2shortcutvalue class="form-control" maxlength=256 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating('', '<input id=d2addbutton type=button value=' + "Lisätä" + ' onclick=addDeskCustomizeString() class="form-control" />');
            x + '</div>';
            setModalContent('xxAddAgent', "Näppäimistön kielten mukauttaminen", 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>' + "Näppäimistön merkkijonoja ei ole määritetty" + '</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>' + "Kirjoita teksti ja kirjoita se etänä napsauttamalla OK. Varmista, että asetat etäkohdistimen oikeaan kohtaan ennen kuin jatkat." + '<div>';
            x += '<textarea id=d2typeText style="margin-top:5px;width:100%;height:184px;resize:none" maxlength=2000>' + (text ? EscapeHtml(text) : '') + '</textarea>';
            setModalContent('xxAddAgent', "Etänäppäimistön merkintä", 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="' + "Hanki leikepöytä" + '" onclick=showDeskClipGet()></div>';
            if ((features2 & 0x1000) == 0) x += '<div class="col"><input class="btn btn-secondary" id=dlgClipSet type=button value="' + "Aseta leikepöytä" + '" 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>' + "Etäleikepöytä on voimassa 60 sekuntia." + '</span></div></div></div>';
            x += '</div>'
            xxdialogTag = 'clipboard';
            setModalContent('xxAddAgent', "Etä leikepöytä", 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="' + "Pysäytä prosessi" + '" 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("Prosessin tiedot, #{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 = "Pysähtyi"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Juoksemassa"; }
                        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("Nimi", service.name); }
                    if (service.displayName) { x += addHtmlValue("Näyttönimi", service.displayName); }
                    if (service.status) {
                        var ss = capitalizeFirstLetter(service.status.state.toLowerCase());
                        if (ss == 'Stopped') { ss = "Pysähtyi"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "Juoksemassa"; }
                        if (service.status.state) { x += addHtmlValue("Tila", ss); }
                        if (service.status.pid) { x += addHtmlValue("PID", service.status.pid); }
                        var serviceTypes = [];
                        if (service.status.isFileSystemDriver === true) { serviceTypes.push("TiedostojärjestelmänAjuri"); }
                        if (service.status.isInteractive === true) { serviceTypes.push("Interaktiivinen"); }
                        if (service.status.isKernelDriver === true) { serviceTypes.push("Ytimenohjain"); }
                        if (service.status.isOwnProcess === true) { serviceTypes.push("OmaProsessi"); }
                        if (service.status.isSharedProcess === true) { serviceTypes.push("Jaettu prosessi"); }
                        if (serviceTypes.length > 0) { x += addHtmlValue("Tyyppi", serviceTypes.join(', ')); }
                    }
                    if (service.startType) { x += addHtmlValue("Start Type", service.startType); }
                    if (service.user) { x += addHtmlValue("Käyttäjä", service.user); }
                    if (service.installedBy) { x += addHtmlValue("Installed By", service.installedBy); }
                    if (service.installedDate) { x += addHtmlValue("Installed Date", printDateTime(new Date(service.installedDate))); }
                    if (service.failureActions) {
                        if (service.failureActions.resetPeriod) {
                            var abc = ((service.failureActions.resetPeriod < 86400 ? 0 : service.failureActions.resetPeriod) / (24 * 60 * 60));
                            x += addHtmlValue("Restart Fail Count After ", abc + ' Day' + (abc != 1 ? 's' : ''));
                        }
                        if (service.failureActions.actions) {
                            if (service.failureActions.actions[0]) { x += addHtmlValue("First Failure", service.failureActions.actions[0].type); }
                            if (service.failureActions.actions[1]) { x += addHtmlValue("Second Failure", service.failureActions.actions[1].type); }
                            if (service.failureActions.actions[2]) { x += addHtmlValue("Subsequent Failures", service.failureActions.actions[2].type); }
                        }
                    }
                    x += '<br/><div style=float:right;margin-bottom:12px><input type=button value="' + "Sulje" + '" onclick=showServiceDetailsDialogEx(0,' + index + ')></div><div style=margin-bottom:12px><input type=button value="' + "Käynnistä" + '" onclick=showServiceDetailsDialogEx(1,' + index + ')><input type=button value="' + "Pysäytä" + '" onclick=showServiceDetailsDialogEx(2,' + index + ')><input type=button value="' + "Käynnistä uudelleen" + '" onclick=showServiceDetailsDialogEx(3,' + index + ')></div>';
                    xxdialogTag = "service_" + index;
                    setModalContent('xxAddAgent', "Palvelun tiedot", 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', "Palvelun tiedot", 'Requesting Service Details...');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
            return false;
        }

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

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

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

        // Save the desktop image to file
        function deskSaveImage() {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            var d = new Date(), n = "Työpöytä" + '-' + 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 = "Kaikki näytöt"; allDisplays = 2; } // Language translation
                if (str.startsWith('Display ')) { str = format("Näyttö {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>' + "Näyttönäppäimistö"
                    + ' <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', "Prosessinhallinta", format("Lopetetaanko prosessi #{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 -portti {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("Todentaminen", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=3 selected>' + "Tallennettu avain" + '</option><option value=1>' + "Käyttäjänimi Salasana" + '</option><option value=2>' + "Käyttäjätunnus ja avain" + '</option></select>');
            } else {
                x += addHtmlFormFloating("Todentaminen", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=1 selected>' + "Käyttäjänimi Salasana" + '</option><option value=2>' + "Käyttäjätunnus ja avain" + '</option></select>');
            }
            x += '<div id=d2userauth style=display:none>';
            x += addHtmlFormFloating("Käyttäjätunnus", '<input id=dp2user class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
            x += '</div>';
            x += '<div id=d2passauth style=display:none>';
            x += addHtmlFormFloating("Salasana", '<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">' + "Muista valtakirjat" + '</label>'); }
            x += '</div><div id=d2keyauth style=display:none>';
            x += addHtmlValue("Avaintiedosto", '<input type=file id=dp2key class="form-control" maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />' + '<div id=d2badkey style=font-size:x-small>' + "Avaintiedoston on oltava OpenSSH -muodossa." + '</div>');
            x += addHtmlFormFloating("Avaimen salasana", '<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)>' + "Muista käyttäjä ja avain" + '</label>');
                x += addHtmlValue('', '<label><input id=dp2keep2 type=checkbox class="form-check-input me-2">' + "Muista salasana" + '</label>');
            }
            x += '</div>';
            if (j.askkeypass) {
                x += '<div id=d2keyauth2 style=display:none>';
                x += addHtmlFormFloating("Salasana", '<input type=password id=dp2keypass2 class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
                x += '</div>';
            }
            setModalContent('xxAddAgent', "Todentaminen", 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("Yhteysvirhe", 5000); break; }
                        case 'autherror': { p12setConsoleMsg("Todennusvirhe", 5000); break; }
                        case 'sessionerror': { p12setConsoleMsg("Istunto päättyi", 5000); break; }
                        case 'sessiontimeout': { p12setConsoleMsg("Istunto vanhentunut", 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-terminaali", "Laajennettu 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)", "Vaihtoehtoinen (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', "Liitä", '<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>' + "Tiedostoja ei löytynyt" + '</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("Todennusvirhe", 5000); return; }
                case 'connectionerror': { p13setConsoleMsg("Yhteysvirhe", 5000); return; }
                case 'sessionerror': { p13setConsoleMsg("Istunto päättyi", 5000); return; }
                case 'sessiontimeout': { p13setConsoleMsg("Istunto vanhentunut", 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', "Tiedoston käyttö", '<div style=margin:10px>' + "Pakataan tiedostoja ..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13fileOperationDialogEx(10));
                } else if ((data.msg == 'unzipping') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Tiedoston käyttö", '<div style=margin:10px>' + "Unzipping file..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else if ((data.msg == 'unziperror') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Tiedoston käyttö", '<div style=margin:10px>' + "Unzipping Error" + '</div><br />' + EscapeHtml(data.error));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                } else if ((data.msg == 'zippingFile') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Tiedoston käyttö", '<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', "Open File/Folder", "Are you sure you want to open this file/folder on the remote devices desktop?");
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13openfilefolderEx());
        }
        function p13openfilefolderEx() {
            var openfilefolder = "", checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) {
                if (checkboxes[i].checked) {
                    openfilefolder = (isWindowsNode(currentNode) ? '' : '/') + p13filetreelocation.join(isWindowsNode(currentNode) ? '\\' : '/') + (isWindowsNode(currentNode) ? '\\' : '/') + p13filetree.dir[checkboxes[i].value].n;
                }
            }
            if (openfilefolder == "") return;
            files.sendText({ action: 'open', reqid: 1, path: openfilefolder, dir: (p13getFileSelDirCount() == 1) ? true : false });
        }

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

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

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

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

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

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

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

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

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

        function p13findfile() {
            if (xxdialogMode) return;
            var x = addHtmlFormFloating("Suodata", '<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=' + "Sulje" + ' style=float:right;width:80px;margin-left:5px onclick=p13findfileCancel()>';
            x += '<input id=filefind_dlgOkButton type=submit value=' + "Etsi" + ' style=float:right;width:80px onclick=p13findfileEx()><br /><br /></div>';
            setModalContent('xxAddAgent', "Etsi tiedostot", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            p13findfileValidate();
            Q('d2findFilter').focus();
        }

        function p13findfileDown(e) { if (e.code == "Tulla sisään") { 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 = "Valitse kaikki";
                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 ? "Poista valinta" : "Valitse kaikki");
                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 -portti {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', "Uusi kansio", '<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', "Uusi tiedosto", '<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>' + "Rekursiivinen poisto" + '</label><br>' : '<input type=checkbox class="form-check-input me-2" id=p13recdeleteinput style=\'display:none\'>';
            setModalContent('xxAddAgent', "Poista", (cc > 1) ? (format("Poista {0} valittua kohdetta?", cc) + rec) : ("Poista valittu kohde?" + 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', "Nimeä uudelleen", '<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', "Lataa tiedosto", '<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("Tiedostoeditori", "Vain alle 200 kt: n tiedostoja voidaan muokata."); }
                    break;
                }
            }
        }

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

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

        function p13zipFiles() {
            var inputFiles = [], checkboxes = document.getElementsByName('fd');
            for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { inputFiles.push(p13filetree.dir[checkboxes[i].value].n); } }
            setModalContent('xxAddAgent', "Zip-tiedostonimi", '<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("Haluatko varmasti kopioida {0} kojdetta tänne?", p13clipboard.length); } else { x = format("Haluatko varmasti kopioida kohteen tänne?"); }
                } else {
                    if (p13clipboard.length > 1) { x = format("Haluatko varmasti siirtää {0} kohdetta tänne?", p13clipboard.length); } else { x = format("Haluatko varmasti siirtää kohteen tänne?"); }
                }
            }
            setModalContent('xxAddAgent', "Liitä", 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} kohdetta valittu kopiointia varten" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Tyhjennä" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Yksi valittu kopiointia varten" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Tyhjennä" + '</a>.');
                    }
                } else {
                    if (p13clipboard.length > 1) {
                        x = format("{0} kohdetta valittu siirtoa varten" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Tyhjennä" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Yksi valittu siirtoa varten" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Tyhjennä" + '</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', "Lataa tiedosto", '<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 = ["Rivitä: PÄÄLLÄ", "Rivitä: POIS PÄÄLTÄ"][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 = ["Koko: 100%", "Koko: 125%", "Koko: 150%", "Koko: 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 = ["Koodaus: RAW", "Koodaus: UTF8"][d4EditEncodingVal];
            putstore('editorEncoding', d4EditEncodingVal);
        }

        function d4ToggleLineBreak(update) {
            if (!update) {
                d4EditLineBreakVal = ++d4EditLineBreakVal % 3;
            }
            Q('d4LineBreakButton').value = ["Rivinvaihto: Windows (CR LF)", "Rivinvaihto: Linux (LF)", "Rivinvaihto: 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', "Lataa tiedosto", format((overWriteCount == 1) ? "Lataus korvaa 1 tiedoston. Jatkaa?" : "Lataus korvaa {0} tiedostoa. Jatkaa?", 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', "Lataa tiedosto", '<div id=p13dfileName>' + "Yhdistetään..." + '</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="' + "Tämä on vieraiden jakamisistunto" + '">' + 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>' + "Ei tapahtumia" + '</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', "Tapahtuman lisätiedot", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
        }

        //
        // DEVICE DETAILS
        //

        var deviceDetailsStats = null;
        var deviceDetailsStatsTimer = null;
        var deviceDetailsStatsData = [];
        var deviceDetailsConfig = {
            type: 'line',
            data: {
                labels: [],
                datasets: [
                    { label: "Prosessori", backgroundColor: 'rgba(134, 16, 158, .5)', borderColor: 'rgb(134, 16, 158)', data: [], fill: true },
                    { label: "Muisti", 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("Nimi", EscapeHtml(node.rname), s); }
            if (hardware.windows && hardware.windows.osinfo && hardware.windows.osinfo.Description) { x += addDetailItem("Kuvaus", EscapeHtml(hardware.windows.osinfo.Description), s); }
            if (node.osdesc) { x += addDetailItem("Versio", EscapeHtml(node.osdesc), s); }
            if (hardware.windows && hardware.windows.osinfo) {
                var m = hardware.windows.osinfo;
				if(m.BuildRevision){ x += addDetailItem("Koontiversion numero", EscapeHtml(m.BuildRevision), s); }
                if (m.OSArchitecture) {
                    if (m.OSArchitecture.startsWith('32')) { x += addDetailItem("Arkkitehtuuri", "32-bittinen", s); }
                    else if (m.OSArchitecture.startsWith('64')) { x += addDetailItem("Arkkitehtuuri", "64-bittinen", s); }
                    else { x += addDetailItem("Arkkitehtuuri", EscapeHtml(m.OSArchitecture), s); }
                }
                if (m.LastBootUpTime) {
                    var thedate = {
                        year: parseInt(m.LastBootUpTime.substring(0, 4)),
                        month: parseInt(m.LastBootUpTime.substring(4, 6)) - 1, // Months are 0-based in JavaScript (0 - January, 11 - December)
                        day: parseInt(m.LastBootUpTime.substring(6, 8)),
                        hours: parseInt(m.LastBootUpTime.substring(8, 10)),
                        minutes: parseInt(m.LastBootUpTime.substring(10, 12)),
                        seconds: parseInt(m.LastBootUpTime.substring(12, 14)),
                    };
                    const date = printDateTime(new Date(thedate.year, thedate.month, thedate.day, thedate.hours, thedate.minutes, thedate.seconds));
                    x += addDetailItem("Last Boot Up Time", date);
                }
                if(m.Domain){ x += addDetailItem((m.PartOfDomain ? "Verkkotunnus" : "Työryhmä"), EscapeHtml(m.Domain), s); }
                if(m.DomainState > 0){ x += addDetailItem("Verkkotunnuksen tila", (domainStates[m.DomainState] ? domainStates[m.DomainState] : "Tuntematon"), s); }
            }
            if (hardware.linux && hardware.linux.LastBootUpTime) {
                var lastBootUpTime = new Date(hardware.linux.LastBootUpTime);
                const date = printDateTime(lastBootUpTime);
                x += addDetailItem("Last Boot Up Time", date);
            }
            if (hardware.linux && hardware.linux.arch) {
                var arch = hardware.linux.arch;
                x += addDetailItem("Arkkitehtuuri", (['x86_64','amd64'].includes(arch) ? "64-bittinen" : ['i686','x86','i586'].includes(arch) ? "32-bittinen" : EscapeHtml(arch)), s);
            }
            if (hardware.linux && hardware.linux.kernel_release) { x += addDetailItem("Ytimen julkaisuversio", EscapeHtml(hardware.linux.kernel_release), s); }
            if (hardware.linux && hardware.linux.kernel_build) { x += addDetailItem("Ytimen koontiversio", EscapeHtml(hardware.linux.kernel_build), s); }
            if (hardware.darwin && hardware.darwin.LastBootUpTime) {
                var lastBootUpTime = new Date(hardware.darwin.LastBootUpTime * 1000); // must times by 1000 even tho timestamp is correct?
                const date = printDateTime(lastBootUpTime);
                x += addDetailItem("Last Boot Up Time", date);
            }
            if (x != '') { sections.push({ name: "Käyttöjärjestelmä", 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 Agentti", str);
                }
                if (node.firstconnect) { x += addDetailItem("Ensimmäinen agenttiyhteys", printDateTime(new Date(node.firstconnect))); }
                if ((node.conn & 1) != 0) {
                    x += addDetailItem("Agentin edellinen yhteys", "Yhdistetty nyt");
                } else {
                    if (node.lastconnect) { x += addDetailItem("Agentin edellinen yhteys", printDateTime(new Date(node.lastconnect))); }
                }
                if (node.lastaddr) {
                    var splitip = node.lastaddr.split(':');
                    if (splitip.length > 2) {
                        // IPv6
                        x += addDetailItem("Agentin edellinen osoite", '<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("Agentin edellinen osoite", splitip[0]);
                        } else {
                            x += addDetailItem("Agentin edellinen osoite", '<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("Käännä aika", isNaN(d) ? EscapeHtml(hardware.agentvers.compileTime) : printDateTime(new Date(d)));
                    }
                }
                if (hardware.time != null) {
                    x += addDetailItem("Viimeisin yksityiskohtien päivitys", printDateTime(new Date(hardware.time)));
                }
                if (x != '') { sections.push({ name: "Mesh Agentti", html: x, img: 'meshagent64.png' }); }
            }

            // Mobile
            if (hardware.mobile) {
                var x = '';
                if (hardware.mobile.brand && hardware.mobile.model) { x += addDetailItem("Malli", EscapeHtml(hardware.mobile.brand + ', ' + hardware.mobile.model), s); }
                if (hardware.mobile.device) { x += addDetailItem("Laite", EscapeHtml(hardware.mobile.device), s); }
                if (hardware.mobile.bootloader) { x += addDetailItem("Käynnistyslatain", EscapeHtml(hardware.mobile.bootloader), s); }
                if (hardware.mobile.id) { x += addDetailItem("Tunniste", EscapeHtml(hardware.mobile.id), s); }
                if (hardware.mobile.host) { x += addDetailItem("Isäntänimi", EscapeHtml(hardware.mobile.host), s); }
                if (hardware.mobile.androidapi && hardware.mobile.androidrelease) { x += addDetailItem("Android Version", EscapeHtml(hardware.mobile.androidrelease + ', API Level ' + hardware.mobile.androidapi), s); }
                if (x != '') { sections.push({ name: "Mobiililaite", 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("Kuvaus", 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 Kerros", format("MAC: {0}, Yhdyskäytävä: {1}", EscapeHtml(m.mac), EscapeHtml(m.gatewaymac)));
                        } else {
                            if (m.mac) { x += addDetailItem("MAC Kerros", 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 Kerros", format("IP: {0}, Peite: {1}, Yhdyskäytävä: {2}", EscapeHtml(ipv4layer.v4addr), EscapeHtml(ipv4layer.v4mask), EscapeHtml(ipv4layer.v4gateway)));
                        } else {
                            if (ipv4layer.v4addr) { x += addDetailItem("IPv4 Kerros", format("IP: {0}", EscapeHtml(ipv4layer.v4addr))); }
                        }
                    }
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Verkko", 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("Kuvaus", 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 Kerros", format("MAC: {0}, Yhdyskäytävä: {1}", EscapeHtml(m[0].mac), EscapeHtml(m[0].gatewaymac)));
                        } else {
                            x += addDetailItem("MAC Kerros", format("MAC: {0}", EscapeHtml(m[0].mac)));
                        }
                    }
                    if (typeof m[0].speed == 'number' && (m[0].speed != 9223372036854775807 && m[0].speed > 0)) {
                        x += addDetailItem("Käyttöliittymän nopeus", 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("Maski: {0}", EscapeHtml(iplayer.netmask))); }
                        if (iplayer.gateway) { items.push(format("Yhdyskäytävä: {0}", EscapeHtml(iplayer.gateway))); }
                        if (items.length > 0) {
                            if (iplayer.family == 'IPv4') { x += addDetailItem("IPv4 Kerros", items.join(", ")); }
                            if (iplayer.family == 'IPv6') { x += addDetailItem("IPv6 Kerros", items.join(", ")); }
                        }
                    }
                    x += '</div></td></tr>';
                }
                if (hardware.network && hardware.network.dns) {
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += addDetailItem('<b>' + "DNS Servers" + '</b>', EscapeHtml(hardware.network.dns.join(", ")));
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Verkko", html: x, img: 'networking64.png' }); }
            }

            // Attribute: Intel AMT
            if (node.intelamt != null) {
                var x = '';
                x += addDetailItem("Versio", (node.intelamt.ver) ? ('v' + EscapeHtml(node.intelamt.ver)) : ('<i>' + "Tuntematon" + '</i>'), s);
                var provisioningStates = { 0: nobreak("Ei aktivoitu"), 1: nobreak("Esiaktivointi"), 2: nobreak("Aktivoitu") };
                var provisioningMode = '';
                if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Asiakasohjaustila (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Järjestelmänvalvojan hallintatila (ACM)"); } }
                x += addDetailItem("Valmistelutila", ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('<i>' + "Tuntematon" + '</i>')) + provisioningMode, s);
                x += addDetailItem("Turvallisuus", (node.intelamt.tls == 1) ? "Suojattu TLS: llä" : "TLS: ää ei ole määritetty", s);
                // Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
                x += addDetailItem("Järjestelmänvalvojan tunnukset", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0))) ? "Ei tunnettu" : "Tunnettu", s);
                if (x != '') {
                    if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
                        sections.push({ name: "Intel&reg; Standard Manageability (Intel&reg; SM)", html: x, img: 'amt64.png' });
                    } else {
                        sections.push({ name: "Intel&reg; Active Management-tekniikka (Inter&reg; AMT)", html: x, img: 'amt64.png' });
                    }
                }
            }

            if (hardware.identifiers) {
                var x = '', ident = hardware.identifiers;
                // BIOS
                if (ident.bios_vendor) { x += addDetailItem("Valmistaja", EscapeHtml(ident.bios_vendor), s); }
                if (ident.bios_version) { x += addDetailItem("Versio", EscapeHtml(ident.bios_version), s); }
                if (ident.bios_serial) { x += addDetailItem("Sarja", EscapeHtml(ident.bios_serial), s); }
                if (ident.bios_mode) { x += addDetailItem("Mode", EscapeHtml(ident.bios_mode), s); }
                if (x != '') { sections.push({ name: "BIOS", html: x, img: 'chip64.png' }); }

                // Motherboard
                x = '';
                if (ident.board_vendor) { x += addDetailItem("Valmistaja", EscapeHtml(ident.board_vendor), s); }
                if (ident.board_name) { x += addDetailItem("Nimi", EscapeHtml(ident.board_name), s); }
                if (ident.board_serial && (ident.board_serial != '')) { x += addDetailItem("Sarja", EscapeHtml(ident.board_serial), s); }
                if (ident.board_version) { x += addDetailItem("Versio", EscapeHtml(ident.board_version), s); }
                if (ident.product_uuid) { x += addDetailItem("Tunniste", EscapeHtml(ident.product_uuid), s); }
                if (ident.cpu_name) { x += addDetailItem("Prosessori", 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: "Emolevy", html: x, img: 'motherboard64.png' }); }

                // System
                x = '';
                if (ident.chassis_manufacturer) { x += addDetailItem("Valmistaja", EscapeHtml(ident.chassis_manufacturer), s); }
                if (ident.product_name) { x += addDetailItem("Tuotteen nimi", EscapeHtml(ident.product_name), s); }
                if (ident.chassis_serial) { x += addDetailItem("Sarja", EscapeHtml(ident.chassis_serial), s); }
                if (ident.chassis_assettag) { x += addDetailItem("Omaisuusmerkintä", EscapeHtml(ident.chassis_assettag), s); }
                if (x != '') { sections.push({ name: "Järjestelmä", html: x, img: 'system64.png' }); }
            }

            // TPM
            if (hardware.tpm) {
                var x = '', tpm = hardware.tpm;
                if (tpm.SpecVersion) { x += addDetailItem("SpecVersion", parseFloat(EscapeHtml(tpm.SpecVersion)).toFixed(1), s); }
                if (tpm.ManufacturerId) { x += addDetailItem("Tunniste", EscapeHtml(tpm.ManufacturerId), s); }
                if (tpm.ManufacturerVersion) { x += addDetailItem("Versio", EscapeHtml(tpm.ManufacturerVersion), s); }
                if (tpm.IsActivated != null) { x += addDetailItem("Aktivoitu", (tpm.IsActivated ? "Yes" : "No"), s); }
                if (tpm.IsEnabled != null) { x += addDetailItem("Käytössä", (tpm.IsEnabled ? "Yes" : "No"), s); }
                if (tpm.IsOwned != null) { x += addDetailItem("Omistettu", (tpm.IsOwned ? "Yes" : "No"), s); }
                if (x != '') { sections.push({ name: "TPM", html: x, img: 'tpm64.png' }); }
            }

            if (hardware.battery) {
                var x = '';
                x += '<table class="table table-hover" style=width:100%>';
                for (var i in hardware.battery) {
                    var battery = hardware.battery[i];
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + EscapeHtml(battery.DeviceName ? battery.DeviceName : "Tuntematon") + '</b></div>';
                    if (battery.CycleCount) { x += addDetailItem("Cycle Count", EscapeHtml(battery.CycleCount), s); }
                    if (battery.FullChargedCapacity) { x += addDetailItem("Täysi ladattu kapasiteetti", format("{0} mWh", battery.FullChargedCapacity), s); }
                    if (battery.EstimatedRuntime) { x += addDetailItem("Arvioitu käyttöaika", format("{0} minuuttia", Math.floor((battery.EstimatedRuntime / 60))), s); }
                    if (battery.Chemistry) { x += addDetailItem("Kemia", EscapeHtml(battery.Chemistry), s); }
                    if (battery.DesignedCapacity) { x += addDetailItem("Suunnittelukapasiteetti", format("{0} mWh", battery.DesignedCapacity), s); }
                    if (battery.ManufactureDate) { x += addDetailItem("Valmistuspäivämäärä", EscapeHtml(battery.ManufactureDate), s); }
                    if (battery.ManufactureName) { x += addDetailItem("Valmistajan nimi", EscapeHtml(battery.ManufactureName), s); }
                    if (battery.SerialNumber) { x += addDetailItem("Sarjanumero", EscapeHtml(battery.SerialNumber), s); }
                    if (battery.ChargeRate) { x += addDetailItem("Maksuprosentti", format("{0} mW", battery.ChargeRate), s); }
                    if (battery.Charging != null) { x += addDetailItem("Lataus", (battery.Charging ? "Yes" : "No"), s); }
                    if (battery.DischargeRate) { x += addDetailItem("Purkausnopeus", format("{0} mW", battery.DischargeRate), s); }
                    if (battery.Discharging != null) { x += addDetailItem("Purkautuminen", (battery.Discharging ? "Yes" : "No"), s); }
                    if (battery.RemainingCapacity) { x += addDetailItem("Jäljellä oleva kapasiteetti", format("{0} mWh", battery.RemainingCapacity), s); }
                    if (battery.Voltage) { x += addDetailItem("Jännite", format("{0} V", (battery.Voltage / 1000)), s); }
                    if (battery.Health) { x += addDetailItem("Terveys", format("{0} %", battery.Health), s); }
                    if (battery.BatteryCharge) { x += addDetailItem("Akun lataus", format("{0} %", battery.BatteryCharge), s); }
                    x += '</div>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Akku", 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("Kapasiteetti / Nopeus", EscapeHtml(format("{0} Mb, {1} MHz", (m.Capacity / 1024 / 1024), m.Speed)), s); }
                        else if (m.Capacity) { x += addDetailItem("Kapasiteetti", EscapeHtml(format("{0} Mb", (m.Capacity / 1024 / 1024))), s); }
                        if (m.PartNumber) { x += addDetailItem("Osa numero", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Muisti", 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("Kapasiteetti / Nopeus", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Kapasiteetti", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Osa numero", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "Muisti", 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("Kapasiteetti / Nopeus", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Kapasiteetti", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Osa numero", EscapeHtml((m.Manufacturer && m.Manufacturer != '') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';
                    if (x != '') { sections.push({ name: "Muisti", 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("Malli", 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("Kapasiteetti", format("{0} Mb", Math.floor(m.Size / 1024 / 1024)), s); }
                            if (typeof m.Size == 'string') { x += addDetailItem("Kapasiteetti", 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("Tila", EscapeHtml(foundObject.Status), s);
                        }
                        x += '</div>';
                    }
                }
                x += '</table>';

                if (x != '') { sections.push({ name: "Tallennustila", 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: "Tuntematon", 1: "Ei juurihakemistoa", 2: "Irrotettava levy", 3: "Paikallinen levy", 4: "Verkkoasema", 5: "CD-levy", 6: "RAM-levy" };
                const conversionStatus = { 1: "Täysin salattu", 2: "Salaus käynnissä", 3: "Salauksen purku käynnissä", 4: "Salaus keskeytetty", 5: "Salauksen purku keskeytetty" };
                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("Kapasiteetti", 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("Jäljellä oleva kapasiteetti", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        if (m.dType != null) { type = driveType.hasOwnProperty(m.dType) ? driveType[m.dType] : "Tuntematon"; }
                        x += addDetailItem("File System", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Tuntematon" : 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 = "Lukittu";
                            } else {
                                bitlockerState = (conversionStatus.hasOwnProperty(m.volumeStatus) ? conversionStatus[m.volumeStatus] : "Tuntematon");
                            }
                        }
                        bitlockerState += (encMethod.hasOwnProperty(m.encryptionMethod) ? ' / ' + encMethod[m.encryptionMethod] : ' / ' + "Tuntematon");
                        var lastrp = (m.identifier && hardware.windows.bitlocker) ? hardware.windows.bitlocker[m.identifier] : null;
                        var legacyrp = (m.lastKnownRecoveryPassword && (m.lastKnownIdentifier === m.identifier)) ? m.lastKnownRecoveryPassword : null;   // pre key-list (un-migrated) docs
                        var sendPW = m.recoveryPassword || (lastrp ? lastrp.rp : null) || legacyrp || "External (PIN/Key/File)";
                        if (m.identifier) {
                            bitlockerState += addKeyLink('', 'deviceDetailsShowBitlockerInfo(\"' + encodeURIComponentEx(i) + '\",\"' + encodeURIComponentEx(m.identifier) + '\",\"' + encodeURIComponentEx(sendPW) + '\"' + ')');
                        }
                        x += addDetailItem("BitLocker", bitlockerState, s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Storage Volumes", html: '<table style=width:100%>' + x + '</table>', img: 'storage64.png' }); }
            }

            // Linux Volumes
            if (hardware.linux && hardware.linux.volumes) {
                var x = '';
                for (var i in hardware.linux.volumes) {
                    var m = hardware.linux.volumes[i];
                    if (m.mount_point.startsWith('/var/lib/docker/overlay2')) continue;
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + m.mount_point + '</b></div>';
                    if (m.size) {
                        var sizes = ['KB', 'MB', 'GB', 'TB'];
                        var j = parseInt(Math.floor(Math.log(Math.abs(m.size)) / Math.log(1024)), 10);
                        var fsize = (j === 0 ? `${m.size} ${sizes[j]}` : `${(m.size / (1024 ** j)).toFixed(2)} ${sizes[j]}`);
                        x += addDetailItem("Kapasiteetti", 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("Jäljellä oleva kapasiteetti", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("File System", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Tuntematon" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Storage Volumes", html: '<table style=width:100%>' + x + '</table>', img: 'storage64.png' }); }
            }

            // MacOS Volumes
            if (hardware.darwin && hardware.darwin.volumes) {
                var x = '';
                for (var i in hardware.darwin.volumes) {
                    var m = hardware.darwin.volumes[i];
                    if (m.mount_point.startsWith('/var/lib/docker/overlay2')) continue;
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + m.mount_point + '</b></div>';
                    if (m.size) {
                        x += addDetailItem("Kapasiteetti", EscapeHtml(m.size), s);
                    }
                    if (m.available) {
                        x += addDetailItem("Jäljellä oleva kapasiteetti", EscapeHtml(m.available), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("File System", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Tuntematon" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Storage Volumes", html: '<table style=width:100%>' + x + '</table>', img: 'storage64.png' }); }
            }

            // Render the sections
            var x = '';
            for (var i in sections) {
                if (sections[i].img == null) {
                    x += '<div class=DevSt style=margin-bottom:3px;margin-left:16px><b>' + sections[i].name + '</b></div><div style=margin-bottom:10px;margin-left:16px>' + sections[i].html + '</div>';
                } else {
                    x += '<table style=width:100%><tr>';
                    x += '<td style=width:64px;vertical-align:top><img src=images/details/' + sections[i].img + ' border=0 width=64 /></td>'; // height=12
                    x += '<td><div class=DevSt style=margin-bottom:3px;margin-left:16px><b>' + sections[i].name + '</b></div><div style=margin-bottom:10px;margin-left:16px>' + sections[i].html + '</div></td>';
                    x += '</tr></table>';
                }
            }

            if (x == '') {
                QH('p17info2', "Tälle laitteelle ei ole tietoja.");
            } else {
                QH('p17info2', x);
            }
        }

        function deviceDetailsShowBitlockerInfo(drive, identifier, password) {
            if (xxdialogMode) return false;
            var x = '<div><p>' + "Tunniste" + '</p><p style=user-select:text;font-weight:bold>' + (identifier ? decodeURIComponent(identifier) : "Tuntematon") + '</p>';
            x += '<p>' + "Recovery Password" + '</p><p style=user-select:text;font-weight:bold>' + (password ? decodeURIComponent(password) : "Tuntematon") + '</p></div>';
            setModalContent('xxAddAgent', decodeURIComponent(drive) + ': ' + "BitLocker Information", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

         // ============================================
        // INSTALLED SOFTWARE (Panel 18)
        // ============================================
        //

        var installedAppsData = { desktop: [], store: [] };
        var installedAppsLoading = false;
        var installedAppsLoadingType = 0;  // 0=idle, 1=desktop, 2=store
        var showStoreApps = false;

        function loadInstalledApps() {
            if (currentNode == null) return;
            if (installedAppsLoading) return;
            installedAppsLoading = true;
            installedAppsLoadingType = 1;  // Desktop Apps laden
            installedAppsData = { desktop: [], store: [] };  // Reset
            QH('p18Status', '<span>' + "Ladataan työpöytäsovelluksia..." + '</span>');
            QH('p18html', '<div style="text-align:center;padding:50px;color:gray">' + "Ladataan asennettua ohjelmistoa..." + '</div>');
            if ((currentNode.conn & 1) == 0) {
                QH('p18html', '<div style="text-align:center;padding:50px;color:#cc0000"><b>' + "Laite on offline-tilassa." + '</b></div>');
                QH('p18Status', '<span>' + "Offline-tilassa" + '</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>' + "VIRHE:" + e.message + '</span>');
                installedAppsLoading = false;
                installedAppsLoadingType = 0;
            }
        }

        function loadStoreApps() {
            if (currentNode == null) return;
            if (installedAppsLoading) return;
            installedAppsLoading = true;
            installedAppsLoadingType = 2;
            QH('p18Status', '<span>' + "Ladataan kauppasovelluksia..." + '</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> ' + "Työpöytäsovellukset" + ' (' + 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">' + "Nimi" + '</th>';
            x += '<th style="text-align:left;width:120px">' + "Versio" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Julkaisija" + '</th>';
            x += '<th style="text-align:left;width:100px">' + "Asennuspäivämäärä" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Sijainti" + '</th>';
            x += '<th style="text-align:center;width:100px">' + "Toiminnot" + '</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 ? "Ei vastaavia sovelluksia" + '"' + EscapeHtml(searchTerm) + '"' : "Ei löydetty työpöytäsovelluksia";
                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 -sovellukset" + ' (' + 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">' + "Nimi" + '</th>';
				x += '<th style="text-align:left;width:120px">' + "Versio" + '</th>';
				x += '<th style="text-align:left;width:250px">' + "Paketti" + '</th>';
				x += '<th style="text-align:center;width:100px">' + "Laajuus" + '</th>';
				x += '<th style="text-align:center;width:80px">' + "Toiminnot" + '</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 ? "Ei vastaavia kauppasovelluksia" + '"' + EscapeHtml(searchTerm) + '"' : "Kauppasovelluksia ei löydetty";
					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="' + "Asennettu vain nykyiselle käyttäjälle" + '">' + "Käyttäjä" + '</span>';
							} else if (app.scope.indexOf('System') >= 0) {
								scopeBadge = '<span class="badge bg-info" title="' + "Asennettu kaikille käyttäjille" + '">' + "Järjestelmä" + '</span>';
							}
							if (app.scope.indexOf('Prov') >= 0) {
								scopeBadge += ' <span class="badge bg-secondary" title="' + "Valmisteltu uusille käyttäjille" + '">' + "Valm" + '</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 = "Työpöytä:" + ' ' + shownDesktop + '/' + totalDesktop;
            if (showStoreApps) {
                statusText += ' | ' + "Kauppa:" + ' ' + 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>' + "VIRHE:" + "Asennuksen poistokomentoa ei ole saatavilla" + '</span>');
                return;
            }
            var dialogContent = '<div style="max-width:500px">';
            dialogContent += '<p><b>' + "Poistetaanko tämä ohjelmisto?" + '</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>' + "Hiljainen asennuksen poisto suoritetaan" + '</i></p>';
            dialogContent += '</div>';
            setModalContent('xxAddAgent', "Poista ohjelmisto", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
                QH('p18Status', '<span>' + "Poistetaan..." + '</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', "Poisto aloitettu. Napsauta Päivitä päivittääksesi"); }, 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>' + "Poistetaanko tämä kauppasovellus?" + '</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', "Poista kauppasovellus", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
				QH('p18Status', '<span>' + "Poistetaan..." + '</span>');
				meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'uninstallstoreapp', value: '"' + cmd.replace(/"/g, '\\"') + '"' });
				setTimeout(function() { QH('p18Status', "Poistokäsky lähetetty. Napsauta Päivitä päivittääksesi"); }, 2000);
			});
		}

        function handleUninstallResponse(data) {
            try {
                var result = JSON.parse(data);
                if (result.success) {
                    QH('p18Status', '<span>✓ ' + "Onnistui" + '</span> ' + "Napsauta Päivitä päivittääksesi");
                } else if (result.error) {
                    QH('p18Status', '<span>✗ ' + "VIRHE:" + 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', ' - ' + "Oma palvelinkonsoli");
                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) ? "Agentti on online-tilassa" : "Agentti on offline-tilassa"
                    if ((consoleNode.conn & 16) != 0) { onlineText += ", MQTT on online-tilassa" }
                    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', "Pääsy Evätty");
                    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>' + "TYÖNTÄÄ" + '&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>' + "Lähetä palvelimen oletusydin" + '</option>' +
                    '<option value=2>' + "Tyhjennä ydin" + '</option>' +
                    '<option value=3>' + "Lataa ydintiedosto" + '</option>' +
                    '<option value=4>' + "Pehmeä irrotusaine" + '</option>' +
                    '<option value=5>' + "Kova irrota agentti" + '</option>' +
                    '<option value=6>' + "Lataa palautusydin" + '</option>' +
                    '<option value=7>' + "Lataa pieni ydin" + '</option>' +
                    '<option value=8>' + "Restart agent service" + '</option>' +
                    '<option value=9>' + "Pakota agenttipäivitys" + '</option></select>';
                setModalContent('xxAddAgent', "Suorita agenttitoiminto", addHtmlFormFloating("Toiminta", 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', "Lataa Mesh Agent Core", '');
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p15uploadCoreEx2());
            d3init();
        }

        function p15uploadCoreEx2() {
            var mode = Q('d3uploadMode').value;
            if (mode == 1) {
                // Upload local mesh agent core
                Q('d3submit').click();
            } else {
                // Upload server mesh agent code
                var files = d3getFileSel();
                if (files.length == 1) { meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'custom', path: d3filetreelocation.join('/') + '/' + files[0] }); }
            }
        }

        //
        // MY ACCOUNT
        //

        function account_viewPreviousLogins() {
            if (xxdialogMode) return;
            xxdialogTag = 'previousLogins';
            setModalContent('xxAddAgent', "Edelliset kirjautumiset", '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', "Hallinnoi tilin kuvaa", 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>' + "Vahvistettu puhelinnumero" + '</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() />' + "Poista puhelinnumero" + '</label></div>';
                setModalContent('xxAddAgent', "Puhelin ilmoitukset", 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>' + "Kirjoita tekstiviestihin pystyvä puhelinnumerosi. Kun numero on vahvistettu, sitä voidaan käyttää kirjautumisen varmentamiseen ja muihin ilmoituksiin.";
                x += '<br /><br /><div style=width:100%;text-align:center>' + "Puhelinnumero:" + ' <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', "Puhelin ilmoitukset", 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>' + "Verified handle" + '</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() />' + "Remove messaging" + '</label></div>';
                setModalContent('xxAddAgent', "Messaging Notifications", 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>' + "Enter your messaging service and handle. Once verified, this server can send you login verification and other notifications." + '<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>' + "Service" + '<td>' + y;
                x += '<tr><td>' + "Handle" + '<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">' + "Join this Discord server to receive notifications." + '</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">' + "Information at Pushover.net" + '</a></div>';
                x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Free service at ntfy.sh" + '</a></div>';
                x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Slack Webhook Setup" + '</a></div>';
                xxdialogTag = 'VerifyMessaging';
                setModalContent('xxAddAgent', "Messaging Notifications", 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'] = "username@server.com"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "User key"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Aihe"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "username@sample.com"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Käyttäjätunnus"; }
            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', "Sähköposti todennus", "Kun käytössä, jokaisessa kirjautumisessa, sinulle annetaan mahdollisuus saada kirjautumistunnus sähköpostiisi turvallisuuden lisäämiseksi." + '<br /><br /><label><input id=email2facheck type=checkbox class="form-check-input me-2" ' + (emailU2Fenabled ? 'checked' : '') + '/>' + "Ota käyttöön kaksi vaihenen todennus sähköpostitse." + '</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', "Todennussovellus", ('<div id=d2optinfo>' + "Ladataan..." + '</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', "Todennussovellus", "Haluatko varmasti poistaa kaksivaiheisen kirjautumisen todennussovelluksen?");
            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', "Todennuslaite", "Vahvistetaanko push -todennuslaitteen poisto?");
                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', "Todennuslaite", '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 = "Valitse laite, joka rekisteröidään push -ilmoitusten todennusta varten. Kun laite on valittu, se pyytää vahvistusta." + '<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("Laite", y);
                    setModalContent('xxAddAgent', "Todennuslaite", 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 = "Kirjoita lisättävän avaimen nimi." + '<br /><br />';
                x += addHtmlFormFloating("Avaimen nimi", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "Avaimeni" + '" onkeyup=account_addhkeyValidate(event,2) />');
            } else if (type == 2) {
                var x = "Kirjoita avaimen nimi, valitse OTP-ruutu ja paina YubiKey &trade; -painiketta." + '<br /><br />';
                x += addHtmlFormFloating("Avaimen nimi", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "Avaimeni" + '" 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', "Lisää suojausavain", 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', "Lisää suojausavain", '<br />' + "Tarkistetaan..." + '<br /><br /><br />');
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            } else if (type == 3) {
                meshserver.send({ action: 'webauthn-startregister', name: name });
                return false;
            }
        }

        function account_removehkey(index) {
            meshserver.send({ action: 'otp-hkey-remove', index: index });
            meshserver.send({ action: 'otp-hkey-get' });
        }

        var loclist = { 'af': "afrikaans", 'sq': "Albanialainen", 'ar': "Arabia (vakio)", 'ar-dz': "Arabia (Algeria)", 'ar-bh': "Arabia (Bahrain)", 'ar-eg': "Arabia (Egypti)", 'ar-iq': "Arabia (Irak)", 'ar-jo': "Arabia (Jordania)", 'ar-kw': "Arabia (Kuwait)", 'ar-lb': "Arabia (Libanon)", 'ar-ly': "Arabia (Libya)", 'ar-ma': "Arabia (Marokko)", 'ar-om': "Arabia (Oman)", 'ar-qa': "Arabia (Qatar)", 'ar-sa': "Arabia (Saudi-Arabia)", 'ar-sy': "Arabia (Syyria)", 'ar-tn': "Arabia (Tunisia)", 'ar-ae': "Arabia (U..E.)", 'ar-ye': "Arabia (Jemen)", 'an': "Aragonian", 'hy': "Armenian", 'as': "Assami", 'ast': "Asturian", 'az': "Azerbaidžanin", 'eu': "Baski", 'bg': "Bulgarialainen", 'be': "Valkovenäjä", 'bn': "Bengali", 'bs': "Bosnia", 'br': "Breton", 'my': "Burmalainen", 'ca': "Katalaani", 'ch': "Chamorro", 'ce': "Tšetšenian", 'zh': "Kiinalainen", 'zh-hk': "Kiinalainen (Hong Kong)", 'zh-cn': "Kiinalainen (PRC)", 'zh-sg': "Kiinalainen (Singapore)", 'zh-tw': "Kiinalainen (Taiwan)", 'cv': "Tšuvassi", 'co': "Korsikalainen", 'cr': "Cree", 'hr': "Kroatialainen", 'cs': "Tshekkiläinen", 'da': "Tanskalainen", 'nl': "Hollantilainen (standardi)", 'nl-be': "Hollantilainen (Belgialainen)", 'en': "Englanti", 'en-au': "Englanti (Australia)", 'en-bz': "Englanti (Belize)", 'en-ca': "Englanti (Kanada)", 'en-ie': "Englanti (Irlanti)", 'en-jm': "Englanti (Jamaika)", 'en-nz': "Englanti (Uusi-Seelanti)", 'en-ph': "Englanti (Filippiinit)", 'en-za': "Englanti (Etelä-Afrikka)", 'en-tt': "Englanti (Trinidad ja Tobago)", 'en-gb': "Englanti (Yhdistynyt kuningaskunta)", 'en-us': "Englanti (Yhdysvallat)", 'en-zw': "Englanti (Zimbabwe)", 'eo': "Esperanto", 'et': "Virolainen", 'fo': "Färsaarelainen", 'fa': "Persia (persia)", 'fj': "Fidzi", 'fi': "Suomalainen", 'fr': "Ranska (standardi)", 'fr-be': "Ranska (Belgia)", 'fr-ca': "Ranska (Kanada)", 'fr-fr': "Ranska (Ranska)", 'fr-lu': "Ranska (Luxemburg)", 'fr-mc': "Ranska (Monaco)", 'fr-ch': "Ranska (Sveitsi)", 'fy': "Friisiläinen", 'fur': "Friulilainen", 'gd': "Gaeli (Skotlantilainen)", 'gd-ie': "Gaeli (Irlantilainen)", 'gl': "Gaelinkielinen", 'ka': "Georgialainen", 'de': "Saksalainen (Standardi)", 'de-at': "Saksalainen (Itävalta)", 'de-de': "Saksalainen (Saksa)", 'de-li': "Saksalainen (Liechtenstein)", 'de-lu': "Saksalainen (Luxemburg)", 'de-ch': "Saksalainen (Sveitsi)", 'el': "Kreikkalainen", 'gu': "Gujurati", 'ht': "Haitilainen", 'he': "Heprealainen", 'hi': "Hindi", 'hu': "Unkarilainen", 'is': "Islantilainen", 'id': "Indonesialainen", 'iu': "Inuktitut", 'ga': "Irlantilainen", 'it': "Italialainen (Standardi)", 'it-ch': "Italialainen (Sveitsi)", 'ja': "Japanilainen", 'kn': "Kannada", 'ks': "Kashmiri", 'kk': "Kazakstan", 'km': "Khmer", 'ky': "Kirghiz", 'tlh': "Klingon", 'ko': "Korealainen", 'ko-kp': "Korealainen (Pohjois-Korea)", 'ko-kr': "Korealainen (Etelä-Korea)", 'la': "Latinalainen", 'lv': "Latvialainen", 'lt': "Liettualainen", 'lb': "Luxemburgilainen", 'mk': "FYRO Makedonialainen", 'ms': "Malaiji", 'ml': "Malajalam", 'mt': "Maltalainen", 'mi': "Maori", 'mr': "Marathi", 'mo': "Moldovalainen", 'nv': "Navajo", 'ng': "Ndonga", 'ne': "Nepali", 'no': "Norjalainen", 'nb': "Norjalainen (Bokmal)", 'nn': "Norjalainen (Nynorks)", 'oc': "Occitan", 'or': "Oriya", 'om': "Oromo", 'fa-ir': "Persia / Iran", 'pl': "Puolalainen", 'pt': "Portugalilainen", 'pt-br': "Portugalilainen (Brasilia)", 'pa': "Punjabi", 'pa-in': "Punjabi (Intia)", 'pa-pk': "Punjabi (Pakistan)", 'qu': "Ketšua", 'rm': "Retoromaani", 'ro': "Romania", 'ro-mo': "Romania (Moldova)", 'ru': "Venäläinen", 'ru-mo': "Venäläinen (Moldavia)", 'sz': "Saamen kieli", 'sg': "Sango", 'sa': "Sanskritin kieli", 'sc': "Sardinialainen", 'sd': "Sindhi", 'si': "Singhalese", 'sr': "Serbia", 'sk': "Slovakian", 'sl': "Sloveeni", 'so': "Somani", 'sb': "Sorbi", 'es': "Espanjalainen", 'es-ar': "Espanjalainen (Argentina)", 'es-bo': "Espanjalainen (Bolivia)", 'es-cl': "Espanjalainen (Chile)", 'es-co': "Espanjalainen (Kolumbia)", 'es-cr': "Espanjalainen (Costa Rica)", 'es-do': "Espanjalainen (Dominikaaninen tasavalta)", 'es-ec': "Espanjalainen (Ecuador)", 'es-sv': "Espanjalainen (El Salvador)", 'es-gt': "Espanjalainen (Guatemala)", 'es-hn': "Espanjalainen (Honduras)", 'es-mx': "Espanjalainen (Meksiko)", 'es-ni': "Espanjalainen (Nicaragua)", 'es-pa': "Espanjalainen (Panama)", 'es-py': "Espanjalainen (Paraguay)", 'es-pe': "Espanjalainen (Peru)", 'es-pr': "Espanjalainen (Puerto Rico)", 'es-es': "Espanjalainen (Espanja)", 'es-uy': "Espanjalainen (Uruguay)", 'es-ve': "Espanjalainen (Venezuela)", 'sx': "Sutu", 'sw': "Swahili", 'sv': "Ruotsalainen", 'sv-fi': "Ruotsalainen (Suomi)", 'sv-sv': "Ruotsalainen (Ruotsi)", 'ta': "Tamili", 'tt': "Tataari", 'te': "Teluga", 'th': "Thai", 'tig': "Tigre", 'ts': "Tsonga", 'tn': "Tswana", 'tr': "Turkkilainen", 'tk': "Turkmenia", 'uk': "Ukrainalainen", 'hsb': "Ylempi sorbi", 'ur': "Urdu", 've': "Venda", 'vi': "Vietnamilainen", 'vo': "Volapuk", 'wa': "Vallonia", 'cy': "Walesin", 'xh': "Xhosa", 'ji': "Jiddiš", 'zu': "Zulu" };
        var loclistex = { 'zh-chs': "Kiinalainen (yksinkertaistettu)", 'zh-cht': "Kiinalainen perinteinen)" };
        function account_showLocalizationSettings() {
            if (xxdialogMode) return false;
            var n = getstore('loctag', 0), y = '';
            var x = '<select id=d2locselect class="form-select"><option value="*">' + "Käytä selaimen kieliasetusta" + '</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 += "Kielen vaihtaminen vaatii sivun päivittämisen." + '<br /><br />';
                var z = '<select id=d2langselect class="form-select"><option value="*">' + "Käytä selaimen kieliasetusta" + '</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("Kieli", z);
            }
            y += addHtmlFormFloating("Päivämäärät ja aika", x);

            if ((userinfo.siteadmin == 0xFFFFFFFF) && (domain == '')) {
                y += '<br /><a rel="noreferrer noopener" target="_blank" href="translator.htm">' + "Auta MeshCentralin kääntämisessä" + '</a>';
            }

            setModalContent('xxAddAgent', "Lokalisointiasetukset", 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 = "Luo väliaikainen käyttäjätunnus ja salasana, joita voidaan käyttää vaihtoehtoisena kirjautumisena tilillesi. Tästä on hyötyä, kun työkalut tai muut palvelut voivat käyttää tiliäsi." + '<br /><br />';
            var options = { 0: "Rajoittamaton", 1: "1 minuutti", 5: "5 minuuttia", 10: "10 minuuttia", 15: "15 minuuttia", 30: "30 minuuttia", 45: "45 minuuttia", 60: "60 minuuttia", 120: "2 tuntia", 240: "4 tuntia", 480: "8 tuntia", 720: "12 tuntia", 960: "16 tuntia", 1440: "24 tuntia", 2880: "2 päivää", 5760: "4 päivää", 10080: "7 päivääs" }
            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="' + "Tunnuksen nimi" + '" onchange=account_createLoginTokenValidate() onkeyup=account_createLoginTokenValidate()>';
            x += '<label for=d2tokenName>' + "Tunnuksen nimi" + '</label>';
            x += '</div>';
            x += addHtmlFormFloating("Vanhentumisaika", '<select class="boxsize form-select" id=d2tokenExpire>' + y + '</select>')

            setModalContent('xxAddAgent', "Luo kirjautumistunnus", 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 />' + "Ilmoitusääni" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyGroupName class="form-check-input me-2" type=checkbox />' + "Näytä laiteryhmän nimi" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Laiteyhteydet" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Laitteen irtikytkennät" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Intel&reg; AMT työpöytä ja sarjaliikenne ilmoitukset" + '</label></div>';

            setModalContent('xxAddAgent', "Ilmoitusasetukset", 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 = "Napsauta OK lähettääksesi varmennusviestin osoitteeseen:" + '<br /><div style=padding:8px><b>' + EscapeHtml(userinfo.email) + '</b></div>' + "Odota muutama minuutti saadaksesi vahvistuksen.";
            setModalContent('xxAddAgent', "Sähköpostivarmistus", 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 = "Vaihda tilisi sähköpostiosoite tästä." + '<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', "Sähköpostiosoitteen vaihto", 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 = "Poista tämä tili kirjoittamalla tilin salasana molempiin alla oleviin ruutuihin ja painamalla 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', "Poista Tili", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                dialogclose(1);
            });

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

        function account_showChangePassword() {
            if (xxdialogMode) return false;
            var x = "Vaihda tilisi salasana kirjoittamalla vanha ja uusi salasana kahdesti alla oleviin kenttiin.";
            if (features & 0x00010000) { " Salasanavihjeitä voidaan käyttää, mutta niitä ei suositella."; }
            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>' + "Vaatimukset: " + 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', "Vaihda Salasana", 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: "Oletus" },
                { 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">' + "Valitse teema" + ':</label>';
            x += '<select id="theme-switcher" class="form-select" onchange="account_switchThemeEx()">';
            // Add "Recent Themes" optgroup
            if (lastThemes.length > 0) {
                x += '<optgroup label="' + "Viimeaikaiset teemat" + '"">';
                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="' + "Kaikki teemat" + '">';
            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', "Vaihda teema", 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', "Uusi laiteryhmä", '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', "Tilin Turvallisuus", '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', "Tilin Turvallisuus", '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 = "Luo uusi laiteryhmä käyttäen alla olevia vaihtoehtoja." + '<br /><br />';
            var localGroupType = '';

            x += addHtmlFormFloating("Nimi", '<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>' + "Paikalliset laitteet, ei agenttia" + '</option>'; }
            if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=103>' + "Mitään agenttilaitteita ei välitetty agentin kautta" + '</option>'; }
            if (features2 & 0x10000) {
                if ((features & 1) == 0) { localGroupType += '<option value=4>' + "IP-KVM / Virtalaite" + '</option>'; }
                if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=104>' + "IP-KVM / Virtalaite välitetty agentin kautta" + '</option>'; }
            }
            x += addHtmlFormFloating("Tyyppi", '<select id=dp2meshtype class=form-select onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=2>' + "Hallitse ohjelmistoagentilla" + '</option><option value=1>' + "Vain Intel&reg; AMT, ei agenttia" + '</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("Relelaite", '<select id=d2devrelay onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select >' + relayDevices2.join('') + '</select>');
                x += '</div>';
            }
            x += addHtmlFormFloating("Kuvaus", '<textarea id=dp2meshdesc maxlength=1024 class=form-control></textarea>');
            x += '<div id=d2ipkvm style=display:none><hr />';
            x += addHtmlFormFloating("Malli", '<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>' + "Verkkovirtakytkin 7" + '</option></select>');
            x += addHtmlFormFloating("Isäntänimi", '<input id=dp2ipkvmhost maxlength=128 placeholder="Hostname" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Käyttäjätunnus", '<input id=dp2ipkvmuser maxlength=128 placeholder="Username" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Salasana", '<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', "Uusi laiteryhmä", 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 == "Tulla sisään") && (Q('dp2meshname').value.length > 0)) { Q('dp2meshtype').focus(); }
            if ((x == 2) && (e != null) && (e.key == "Tulla sisään")) { 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>' + "Vahva" + '<span>'; } else if (passStrength >= 60) { r = '<span style=color:blue>' + "Hyvä" + '<span>'; } else { r = '<span style=color:red>' + "Heikko" + '<span>'; }
                } else {
                    // Password requirements provided, use that
                    var passReq = checkPasswordRequirements(Q('apassword1').value, passRequirements);
                    if (passReq == false) { ok = false; r = '<span style=color:red>' + "Politiikka" + '<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 = "Osittaiset oikeudet";
                if (meshrights == 0xFFFFFFFF) rights = "Täysi Järjestelmänvalvoja"; else if (meshrights == 0) rights = "Ei oikeuksia";

                // 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>' + "Aktiiviset kirjautumistunnukset" + '</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> ' + "Uusi" + '</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>' + "Nimi" + '</th><th scope=col style=text-align:left>' + "Käyttäjätunnus" + '</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="' + "Poista kirjautumistunnus" + '" role=button><i class="fa-solid fa-trash text-danger fa-xs"></i></a>';
                    var details = '';
                    if (ltoken.expire != 0) { details = EscapeHtml(format("Vanhenee {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 = "Vahvista tämän kirjautumistunnuksen poisto?" + '<br /><br />';
            x += addHtmlValue("Nimi", EscapeHtml(token.name));
            x += addHtmlValue("Käyttäjätunnus", EscapeHtml(token.tokenUser));
            setModalContent('xxAddAgent', "Poista kirjautumistunnus", 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>' + "Määritä tämä palvelin lataamaan varmuuskopiot automaattisesti Google Driveen. Aloita luomalla ja syöttämällä Google Drive ClientID ja ClientSecret tilillesi." + '</div><br />';
                x += addHtmlValue("Valtakirjat", '<a href="https://console.developers.google.com/apis/api/drive.googleapis.com/credentials" rel="noreferrer noopener" target="_blank">' + "Google Drive Console" + '</a>');
                x += addHtmlFormFloating("Asiakastunnus", '<input id=gdclientid class="form-control" placeholder="xxxxxxxx.apps.googleusercontent.com" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                x += addHtmlFormFloating("Asiakkaan salaisuus", '<input id=gdclientsecret class="form-control" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                setModalContent('xxAddAgent', "Google Drive -varmuuskopio", 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 Driven varmuuskopiointi on tällä hetkellä aktiivista." + '</div>';
                x += '<br /><label><input id=gdcheck type=checkbox class="form-check-input me-2" onchange=server_setupGoogleDriveBackupCheck2() />' + "Poista kokoonpano" + '</label>';
                setModalContent('xxAddAgent', "Google Drive -varmuuskopio", 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 = "Anna valinnainen salasana varmuuskopiota varten" + '<br /><br />';
            x += '<tr><td align=right>' + nobreak("Salasana: ") + '</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') + ' /> ' + "Käytä järjestelmän salasanaa" + '</label></div>';
            setModalContent('xxAddAgent', "Lataa palvelimen varmuuskopio", 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 = "Palauta palvelin varmuuskopiosta," + ' <span style=color:red>' + "tämä poistaa nykyiset palvelintiedot." + '</span> ' + "Tee tämä vain, jos tiedät mitä teet." + '<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("Varmuuskopion salasana: ") + '</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') + ' /> ' + "Käytä järjestelmän salasanaa" + '</label></div>';
            setModalContent('xxAddAgent', "Palauta palvelin", 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', "Palauta palvelin", "Ladataan varmuuskopiota");
            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', "Palauta palvelin", "Verkkovirhe: " + response.status);
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        QE('idx_dlgOkButton', true);
                    }
                })
                .catch((error) => {
                    setModalContent('xxAddAgent', "Palauta palvelin", "Virhe: " + 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 Versio", "Ladataan...");
            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 != "Tuntematon") && (xxdialogTag.current != xxdialogTag.stable) && (latestCheck == false)));
            QE('d2updateCheck2', ((xxdialogTag.latest != "Tuntematon") && (xxdialogTag.current != xxdialogTag.latest) && (stableCheck == false)));
            QE('idx_dlgOkButton', ((stableCheck) && (!latestCheck)) || ((!stableCheck) && (latestCheck)));
        }

        function server_showVersionDlgEx(b, tags) {
            if (Q('d2updateCheck1').checked) { meshserver.send({ action: 'serverupdate', tag: 'stable', version: tags.stable }); }
            if (Q('d2updateCheck2').checked) { meshserver.send({ action: 'serverupdate', tag: 'latest', version: tags.latest }); }
        }

        function server_showErrorsDlg() {
            if (xxdialogMode) return false;
            xxdialogMode = 2; xxdialogTag = 'MeshCentralServerErrors';
            setModalContent('xxAddAgent', "Server Errors", "Ladataan...");
            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>' + "Ei mitään" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span role=button tabindex=0 title="' + "Muokkaa laiteryhmän nimeä napsauttamalla tätä" + '" 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("Tuntematon #{0}", currentMesh.mtype);
            if (currentMesh.mtype == 1) meshtype = "Vain Intel&reg; AMT, ei agenttia";
            if (currentMesh.mtype == 2) meshtype = "Hallinnoidaan ohjelmistoagentin avulla";
            if (currentMesh.mtype == 3) { if (currentMesh.relayid == null) { meshtype = "Paikalliset laitteet, ei agenttia"; } else { meshtype = "Mitään agenttilaitteita ei välitetty agentin kautta"; } }
            if (currentMesh.mtype == 4) { if (currentMesh.relayid == null) { meshtype = "IP-KVM-laite"; } else { meshtype = "IP-KVM-laite välitetty agentin kautta"; } if (currentMesh.kvm.model == 1) { meshtype += ', ' + 'Raritan KX III'; } }

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

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

            // Display IP-KVM information if needed
            if (currentMesh.mtype == 4) {
                x += addHtmlValue("Isäntänimi", currentMesh.kvm.host);
                x += addHtmlValue("Käyttäjätunnus", currentMesh.kvm.user);
            }

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

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

            // Display features
            if (currentMesh.mtype != 3) {
                var meshFeatures = [];
                if (currentMesh.flags) {
                    if (currentMesh.flags & 1) { meshFeatures.push("Automaattinen-Poisto"); }
                    if (currentMesh.flags & 2) { meshFeatures.push((currentMesh.mtype == 4) ? "Portin nimen synkronointi" : "Isäntänimen synkronointi"); }
                    if (currentMesh.flags & 8) { meshFeatures.push("suosi --agentname"); }
                    if (currentMesh.flags & 16) { meshFeatures.push("salli ohitus"); }
                    if ((serverinfo.devGroupSessionRecording == 1) && (currentMesh.flags & 4)) { meshFeatures.push("Tallenna istunnot"); }
                }
                if ((typeof currentMesh.expireDevs == 'number') && (currentMesh.expireDevs > 0)) { meshFeatures.push("Poista passiivinen"); }
                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Ei mitään" + '</i>'; }
                x += addHtmlValue("Ominaisuudet", 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("Työpöydän kehote+työkalurivi"); } else if (consent & 0x0040) { meshFeatures.push("Työpöydän työkalurivi"); } else if (consent & 0x0008) { meshFeatures.push("Työpöydän kehote"); } else { if (consent & 0x0001) { meshFeatures.push("Työpöytä ilmoitus"); } }
                if (consent & 0x0010) { meshFeatures.push("Terminaalikehote"); } else { if (consent & 0x0002) { meshFeatures.push("Ilmoita terminaalista"); } }
                if (consent & 0x0020) { meshFeatures.push("Tiedostot kysy"); } else { if (consent & 0x0004) { meshFeatures.push("Tiedostot Ilmoita"); } }
                if (consent == 7) { meshFeatures = ["Ilmoita aina"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Kysy Aina"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Ei mitään" + '</i>'; }
                x += addHtmlValue("Käyttäjän suostumus", 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("Yhdistä"); }
                    if (meshNotify & 4) { meshNotifyStr.push("Katkaise yhteys"); }
                    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("Email ({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("Messaging ({0})", xx)); }
                    }
                    if (meshNotifyStr.length == 0) { meshNotifyStr.push('<i>' + "Ei mitään" + '</i>'); }
                    x += addHtmlValue("Ilmoitukset", addLink(meshNotifyStr.join(', '), 'p20editMeshNotify()'));
                }
            }

            // Display invitation codes
            if ((features & 0x01000000) && (currentMesh.mtype == 2)) {
                var inviteCodeStr = '<i>' + "Ei mitään" + '</i>', icodes = false;
                if (currentMesh.invite != null) { icodes = true; inviteCodeStr = currentMesh.invite.codes.join(', '); /* + ', ' + currentMesh.invite.flags;*/ }
                x += addHtmlValue("Kutsu koodit", 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 = "Ei politiikkaa";
                if (currentMesh.amt) {
                    if (currentMesh.amt.type == 1) { intelAmtPolicy = "Poista käytöstä"; }
                    else if (currentMesh.amt.type == 2) {
                        intelAmtPolicy = "Yksinkertainen asiakkaan hallintatila (ACM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 3) {
                        intelAmtPolicy = "Yksinkertainen järjestelmänvalvojan hallintatila (ACM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 4) {
                        intelAmtPolicy = "Täysin automaattinen";
                    }
                }
                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=' + "Muistiinpanot" + ' title="' + "Näytä tämän laiteryhmän huomautukset" + '" 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> ' + "Lisää Käyttäjiä" + '</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> ' + "Lisää Käyttäjä Ryhmä" + '</button>'; }
                }
            }

            //if (meshrights & 4) { }
            if (currentMesh.mtype == 1) {
                if (meshrights & 1) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" title="' + "Import Intel&reg; AMT devices." + '" onclick=\'return showAmtImport("' + currentMesh._id + '")\'><i class="fa-solid fa-circle-arrow-down"></i> ' + "Import" + '</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="' + "Suorita Intel&reg; AMT -aktivointi ja -määritykset." + '" onclick=\'return showAmtSetup("' + currentMesh._id + '")\'><i class="fa-solid fa-gear"></i> ' + "Perustaa" + '</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="' + "Lisää uusi tietokone tähän laiteryhmään asentamalla MeshAgent." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Lisää agentti" + '</button>';
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick=\'return inviteAgentToMesh("' + currentMesh._id + '")\' title="' + "Kutsu joku asentamaan Mesh agentti tähän laiteryhmään." + '"><i class="fa-solid fa-user-plus"></i> ' + "Kutsu" + '</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="' + "Lisää laite, joka sijaitsee paikallisessa verkossa." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Lisää Laite" + '</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="' + "Vaihda Intel AMT järjestelmänvalvojan ohjaustilaan (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>' + "Käyttäjäluvat" + '</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="' + "Posta käyttäjältä oikeudet tähän laiteryhmään" + '"><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="' + "Käyttäjä" + '" 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>' + "Aktiivinen laitteen jakaminen" + '</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="' + "Laitteen jakolinkki" + '" 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="' + "Poista laitteen jakaminen" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                    var type = ''; if (dshare.p <= 7) { type = ['', "Pääte", "Työpöytä", "Työpöytä + pääte", "Tiedostot", "Pääte + tiedostot", "Työpöytä + tiedostot", "Työpöytä + pääte + tiedostot"][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} minuutin ajan", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        } else {
                            details = format("{0}, {1} {2} minuutin ajan", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        }
                    }
                    if (dshare.recurring == 1) { details += ", Toistuu päivittäin"; }
                    if (dshare.recurring == 2) { details += ", Toistuu viikoittain"; }
                    if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Näytä vain työpöytä"; }
                    if (dshare.consent != null) {
                        if (dshare.consent == 0) { details += ", Ei suostumusta"; } else {
                            if ((dshare.consent & 0x0038) != 0) { details += ", Pyydä suostumusta"; }
                            if ((dshare.consent & 0x0040) != 0) { details += ", Työkalupalkki"; }
                        }
                    }
                    var guestName = EscapeHtml(dshare.guestName);
                    if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Agentin omavastuu" + '</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="' + "Lataa laiteluettelo" + '" src="images/link4.png" /></a>';
                if ((features & 1) == 0) { dllist += ' <a onclick=meshImportDeviceList()><img title="' + "Tuo laiteluettelo" + '" 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>' + "Laitteet" + '</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>' + "Ei mitään" + '</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>' + "Poista Ryhmä" + '</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 = "Tuo luettelo paikallisista Intel&reg; AMT -laitteista MeshCommander JSON -muodossa." + '<br /><br /><input type=file id=d4importFile class="form-control" accept=".json" onchange=meshImportDeviceListValidate() />';
            setModalContent('xxAddAgent', "Tuo Intel&reg; AMT -laitteet", 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', "Tuo Intel&reg; AMT -laitteet", format("JSON-tiedosto {0} on virheellinen.", 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', "Käyttäjätilin tuonti", 'Unable to import any device.');
                    }
                } else {
                    setModalContent('xxAddAgent', "Tuo Intel&reg; AMT -laitteet", '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>' + "Yksinkertainen järjestelmänvalvojan hallintatila (ACM)" + '</option>'; }
            x += addHtmlFormFloating("Tyyppi", '<select id=dp20amtpolicy class="form-select" onchange=p20editMeshAmtChange()><option value=0>' + "Ei politiikkaa" + '</option><option value=1>' + "Poista käytöstä" + '</option><option value=2>' + "Yksinkertainen asiakkaan hallintatila (ACM)" + '</option>' + acmoption + '<option value=4>' + "Täysin automaattinen" + '</option></select>')
            x += '<div id=dp20amtpolicydiv></div>';

            setModalContent('xxAddAgent', "Intel&reg; AMT politiikka", 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>' + "Säilytä nykyinen salasana" + '</option>' : '';
                x += addHtmlFormFloating("Salasana", '<select id=dp20amtpass class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Satunnaista salasana" + '</option>' + keeppass + '<option value=2>' + "Valitse uusi salasana" + '</option></select>')
                x += '<div id=dp20amtpassdiv style=display:none>';
                x += addHtmlFormFloating("Uusi salasana*", '<input id=dp20amtpolicypass type=password class="form-control" maxlength=32 placeholder="New password*" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy() autocomplete=off />');
                x += addHtmlFormFloating("Uusi salasana*", '<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-tila", '<select id=dp20amtccmmode class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Älä muuta, säilytä CCM, jos se on määritetty" + '</option><option value=1>' + "Poista CCM käytöstä, jos asennus on valmis" + '</option><option value=2>' + "Aktivoi CCM: ksi, jos ACM epäonnistuu" + '</option></select>'); }
                x += '<div id=dp20amtbadpassdiv style=display:none>';
                x += addHtmlFormFloating("Tuntematon salasana", '<select id=dp20amtbadpass class="form-select"><option value=0>' + "Älä tee mitään" + '</option><option value=1>' + "Jos olet CCM: ssä, aktivoi Intel&reg; AMT uudelleen" + '</option></select>');
                x += '</div>';
                if ((features & 0x400) == 0) { x += addHtmlFormFloating("CIRA-asetukset", '<select id=dp20amtcira class="form-select" title="' + "Asiakkaan aloittama etäkäyttö" + '"><option value=0>' + "Älä tee mitään" + '</option><option value=1>' + "Älä muodosta yhteyttä palvelimeen" + '</option><option value=2>' + "Yhdistä palvelimeen" + '</option></select>'); }
                x += '<span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 merkkiä, 1 ylempi, 1 alempi, 1 numeerinen, 1 ei-aakkosnumeerinen." + '</span>';
                if ((currentMesh.mtype == 2) && (ptype == 2)) { x += '<span style="font-size:10px"> ' + "Tämä käytäntö ei vaikuta laitteisiin, joissa on Intel&reg; AMT ACM-tilassa." + '</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">' + "Kun tämä käytäntö on valittu, tämä palvelin ei hallinnoi Intel&reg; AMT: tä. Intel AMT: tä voidaan edelleen käyttää aktivoimalla ja määrittämällä se manuaalisesti." + '</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">' + "Kun tämä käytäntö on valittu, kaikki Intel&reg; AMT Client Control -tilassa (CCM) poistetaan käytöstä. Muissa laitteissa CIRA on tyhjennetty, ja niitä voidaan silti hallita manuaalisesti." + '</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">' + "Tämä on suositeltava käytäntö. Intel&reg; AMT: n aktivointi ja hallinta on täysin automatisoitu, ja palvelin yrittää hyödyntää laitteiston hallintaa parhaalla mahdollisella tavalla." + '</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("Haluatko varmasti poistaa ryhmän {0}? Laiteryhmän poistaminen poistaa myös kaikki tämän ryhmän laitteiden tiedot.", EscapeHtml(currentMesh.name)) + '<br /><br />';
            x += '<label class=form-check-label><input id=p20check type=checkbox class="form-check-input m-2" onchange=p20validateDeleteMeshDialog() />' + "Varmista" + '</label>';
            setModalContent('xxAddAgent', "Poista Ryhmä", 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 = "Relelaitteita ei ole saatavilla.";
                setModalContent('xxAddAgent', "Muokkaa laiteryhmää", "Relelaitteita ei ole saatavilla.");
                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("Relelaite", '<select id=d2devrelay class="form-select">' + relayDevices2.join('') + '</select>');
                setModalContent('xxAddAgent', "Muokkaa laiteryhmää", 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="' + "Nimi" + '" onchange=p20editmeshValidate() onkeyup=p20editmeshValidate(event) />';
            x += '<label for=dp20meshname>' + "Nimi" + '</label>';
            x += '</div>';

            x += '<div class="form-floating mb-2">';
            x += '<textarea id=dp20meshdesc class="form-control" maxlength=1024 placeholder="' + "Kuvaus" + '"></textarea>';
            x += '<label for=dp20meshdesc>' + "Kuvaus" + '</label>';
            x += '</div>';
            setModalContent('xxAddAgent', "Muokkaa laiteryhmää", 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 = "Muokkaa laiteryhmän käyttäjän suostumusta"; }
            if (editType == 2) { consent = (currentUser.consent) ? currentUser.consent : 0; title = "Muokkaa käyttäjän suostumusta"; }
            if (editType == 3) { consent = (currentNode.consent) ? currentNode.consent : 0; title = "Muokkaa laitteen käyttäjän suostumusta"; }
            if (editType == 4) { consent = (currentUserGroup.consent) ? currentUserGroup.consent : 0; title = "Muokkaa käyttäjäryhmän käyttäjän suostumusta"; }
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px"><b>' + "Työpöytä" + '</b></div>';
            x += '<div><label for=d20flag1><input type=checkbox id=d20flag1 class="form-check-input me-2" ' + ((consent & 0x0001) ? 'checked' : '') + '>' + "Ilmoita käyttäjälle" + '</label></div>';
            x += '<div><label for=d20flag2><input type=checkbox id=d20flag2 class="form-check-input me-2" ' + ((consent & 0x0008) ? 'checked' : '') + '>' + "Pyydä käyttäjän suostumusta" + '</label></div>';
            x += '<div><label for=d20flag7><input type=checkbox id=d20flag7 class="form-check-input me-2" ' + ((consent & 0x0040) ? 'checked' : '') + '>' + "Näytä yhteystyökalurivi" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Pääte" + '</b></div>';
            x += '<div><label for=d20flag3><input type=checkbox id=d20flag3 class="form-check-input me-2" ' + ((consent & 0x0002) ? 'checked' : '') + '>' + "Ilmoita käyttäjälle" + '</label></div>';
            x += '<div><label for=d20flag4><input type=checkbox id=d20flag4 class="form-check-input me-2" ' + ((consent & 0x0010) ? 'checked' : '') + '>' + "Pyydä käyttäjän suostumusta" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Tiedostot" + '</b></div>';
            x += '<div><label for=d20flag5><input type=checkbox id=d20flag5 class="form-check-input me-2" ' + ((consent & 0x0004) ? 'checked' : '') + '>' + "Ilmoita käyttäjälle" + '</label></div>';
            x += '<div><label for=d20flag6><input type=checkbox id=d20flag6 class="form-check-input me-2" ' + ((consent & 0x0020) ? 'checked' : '') + '>' + "Pyydä käyttäjän suostumusta" + '</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' : '') + '>' + "Tallenna istunnot" + '</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) ? "Synkronoi palvelimen laitteen nimi portin nimeen" : "Synkronoi palvelimen laitteen nimi isäntänimeen") + '</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' : '') + '>' + "Suosi --agentName-arvoa" + '</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' : '') + '>' + "Salli palvelinlaitteen nimen ohittaminen seuraavaan yhteyteen asti" + '</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' : '') + '>' + "Poista laite kun yhteys kaitkaistaan" + '</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' : '') + '>' + "Poista passiiviset laitteet automaattisesti" + '</label><br></div>';
            x += '<div style=margin-left:20px id=d20expireDeviceDev>' + "Poista päivät käytöstä poistamisen jälkeen" + ' <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>' + "Oletuksena ei -aktiiviset laitteet poistetaan 1 päivän kuluttua." + '</span>', serverinfo.autoremoveinactivedevices);
                } else {
                    x += format('<span style=font-size:x-small;margin-left:6px>' + "Oletuksena ei -aktiiviset laitteet poistetaan {0} päivän kuluttua." + '</span>', serverinfo.autoremoveinactivedevices);
                }
            }

            setModalContent('xxAddAgent', "Muokkaa laiteryhmän ominaisuuksia", 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 += "Salli käyttäjien hallita tätä laiteryhmää ja tämän ryhmän laitteita."; } else { x += "Salli käyttäjien hallita tätä laitetta."; }
                    if (features & 0x00080000) { x += " Käyttäjien on kirjauduttava sisään tähän palvelimeen kerran, ennen kuin heidät voidaan lisätä laiteryhmään." }
                    x += '<br /><br />';
                }
                x += '<div style=\'position:relative\'>';
                x += addHtmlFormFloating("Käyttäjätunnukset", '<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("Laiteryhmä", '<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("Käyttäjäryhmä", '<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("Käyttäjäryhmä", '<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("Laiteryhmä", '<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("Laiteryhmä", '<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("Laite", '<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("Ryhmän käyttöoikeudet {0}:lle.", uname) + '<br /><br />';
                } else {
                    x += format("Ryhmän käyttöoikeudet käyttäjälle {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>' + "Täysi Järjestelmänvalvoja" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editmesh>' + "Muokkaa laiteryhmää" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20manageusers>' + "Hallitse laiteryhmän käyttäjiä" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20managecomputers>' + "Hallitse laiteryhmän tietokoneita" + '</label><br>';
            }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotecontrol>' + "Remote Control & Relay" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remoteview style=margin-left:12px>' + "Vain etäkatselu" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotelimitedinput style=margin-left:12px>' + "Vain rajoitettu syöttö" + '</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>' + "Vieraiden jakaminen" + '</label><br>'; }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nodesktop style=margin-left:12px>' + "Ei työpöytä yhteyttä" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noterminal style=margin-left:12px>' + "Ei pääsyä terminaaliin" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nofiles style=margin-left:12px>' + "Ei tiedostojen käyttöä" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noamt style=margin-left:12px>' + "Ei Intel&reg; AMT" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshagentconsole>' + "Mesh Agentint pääte" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshserverfiles>' + "Palvelimen Tiedostot" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20wakedevices>' + "Herätä laitteet" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editnotes>' + "Muokkaa laitehuomautuksia" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20limitevents>' + "Näytä vain omat tapahtumat" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20chatnotify>' + "Keskustele ja ilmoita" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20uninstall>' + "Poista Agent / Poista laite" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20commands>' + "Etäkomennot" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20resetoff>' + "Nollaa / sammuta" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20details>' + "Laitteen tiedot" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20relay>' + "Use as Relay" + '</label><br>';
            x += '</div>';

            if (userid == null) {
                xxdialogButtons = 3; xxdialogTag = null;
                setModalContent('xxAddAgent', "Lisää käyttäjiä laiteryhmään", 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) ? "Lisää laiteryhmän käyttöoikeudet" : "Muokkaa laiteryhmän käyttöoikeuksia", 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', "Lisää Käyttäjä Ryhmä", 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) ? "Lisää laiteryhmä" : "Muokkaa laiteryhmää", 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) ? "Lisää laitteen käyttöoikeudet" : "Muokkaa laitteen käyttöoikeuksia", 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) ? "Lisää laitteen käyttöoikeudet" : "Muokkaa laitteen käyttöoikeuksia", 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 ? "Muokkaa käyttäjän laitteen käyttöoikeuksia" : "Lisää käyttäjän laitteen käyttöoikeudet", 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 ? "Muokkaa käyttäjäryhmän laiteoikeuksia" : "Lisää käyttäjäryhmän laiteoikeudet", 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', "Muokkaa laiteryhmän käyttöoikeuksia", x);
                } else {
                    xxdialogButtons = 7, xxdialogTag = userid;
                    setModalContent('xxAddAgent', "Muokkaa käyttäjän laiteryhmän oikeuksia", 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("Täysi Järjestelmänvalvoja (kaikki oikeudet)"); else {
                    if ((meshrights & 1) != 0) r.push("Muokkaa laiteryhmää");
                    if ((meshrights & 2) != 0) r.push("Hallitse laiteryhmän käyttäjiä");
                    if ((meshrights & 4) != 0) r.push("Hallitse laiteryhmän tietokoneita");
                    if ((meshrights & 8) != 0) r.push("Etähallinta");
                    if ((meshrights & 16) != 0) r.push("Agentin konsoli");
                    if ((meshrights & 32) != 0) r.push("Palvelimen Tiedostot");
                    if ((meshrights & 64) != 0) r.push("Herätä laitteet");
                    if ((meshrights & 128) != 0) r.push("Muokkaa muistiinpanoja");
                    if (((meshrights & 8) != 0) && (meshrights & 256) != 0) r.push("Vain etäkatselu");
                    if (((meshrights & 8) != 0) && (meshrights & 65536) != 0) r.push("Ei työpöytää");
                    if (((meshrights & 8) != 0) && (meshrights & 512) != 0) r.push("Ei päätettä");
                    if (((meshrights & 8) != 0) && (meshrights & 1024) != 0) r.push("Ei tiedostoja");
                    if (((meshrights & 8) != 0) && (meshrights & 2048) != 0) r.push("Ei Intel&reg; AMT");
                    if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("Rajoitettu syöttö");
                    if ((meshrights & 8192) != 0) r.push("Vain itse tapahtumia");
                    if ((meshrights & 16384) != 0) r.push("Keskustele ja ilmoita");
                    if ((meshrights & 32768) != 0) r.push("Asennuksen poistaminen");
                    if ((meshrights & 131072) != 0) r.push("Komennot");
                    if ((meshrights & 262144) != 0) r.push("Nollaa / Pois");
                    if ((meshrights & 524288) != 0) r.push("Jakaminen");
                    if ((meshrights & 1048576) != 0) r.push("Tarkemmat tiedot");
                    if ((meshrights & 2097152) != 0) r.push("Rele");
                }
                if (r.length == 0) { r.push("Ei oikeuksia"); }
                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("Käyttäjänimi", EscapeHtml(decodeURIComponent(uname)));
                if (xuserid.split('/')[2] != uname) { x += addHtmlValue("Käyttäjätunnus", EscapeHtml(xuserid.split('/')[2])); }

                x += addHtmlValue("Käyttöoikeudet", r.join(", "));
                if (((userinfo._id) != xuserid) && (cmeshrights == 0xFFFFFFFF || (((cmeshrights & 2) != 0) && (meshrights != 0xFFFFFFFF)))) buttons += 4;
                setModalContent('xxAddAgent', "Laiteryhmän käyttäjä", 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', "Poista käyttäjän oikeudet", format("Haluatko varmasti poistaa käyttäjän \"{0}\" oikeudet?", EscapeHtml(decodeURIComponent(uname))));
            }
            if (userid.startsWith('ugrp/')) {
                setModalContent('xxAddAgent', "Poista käyttäjäryhmien käyttöoikeudet", format("Haluatko varmasti poistaa käyttäjäryhmän \"{0}\" oikeudet?", 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 = "Kun tämä on käytössä, kuka tahansa voi käyttää kutsukoodeja laitteiden liittämiseen tähän laiteryhmään seuraavan julkisen linkin avulla:" + '<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() />' + "Ota kutsukoodit käyttöön" + '</label></div>';
                x += addHtmlFormFloating("Kutsu koodit", '<input id=agentInviteCode class="form-control" onkeyup=p20editmeshInviteCodeValidate() placeholder="code1, code2, code3" />');
                x += addHtmlFormFloating("Agentit", '<select id=agentType class="form-select" onchange=p20editmeshInviteCodeValidate()><option value=0>' + "Kaikki käytettävissä olevat edustajat" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "MeshCentral Assistant" + '</option></select>');
                x += '<div id=d2agentInstallTypeDiv>';
                x += addHtmlFormFloating("Asennustyyppi", '<select id=agentInviteType class="form-select"><option value=0>' + "Tausta ja vuorovaikutteinen" + '</option><option value=2>' + "Vain tausta" + '</option><option value=1>' + "Vain interaktiivinen" + '</option></select>');
                x += '</div>';
                setModalContent('xxAddAgent', "Kutsu koodit", 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 = "Kuka tahansa voi käyttää kutsukoodeja laitteiden liittämiseen tähän laiteryhmään seuraavan julkisen linkin avulla:" + '<br /><br />';
                x += '<div style=width:100%;text-align:center><a rel="noreferrer noopener" target=_blank href="' + url + '">' + url + '</a></div><br />';
                x += addHtmlValue("Kutsu koodit", currentMesh.invite.codes.join(', '));
                x += addHtmlValue("Asennustyyppi", ["Tausta ja vuorovaikutteinen", "Vain interaktiivinen", "Vain tausta"][currentMesh.invite.flags & 3]);
                setModalContent('xxAddAgent', "Kutsu koodit", 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">' + "Web-sivun ilmoitukset" + '</div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Laiteyhteydet" + '</label></div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Laitteen irtikytkennät" + '</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 työpöytä ja sarjaliikenne ilmoitukset" + '</label></div>';
            }
            if (emailNotify) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "sähköposti-ilmoitukset" + '</div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Laiteyhteydet" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Laitteen irtikytkennät" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceHelp class="form-check-input me-2" type=checkbox />' + "Help requests" + '</label></div>';
            }
            if ((userinfo.msghandle != null) && ((features2 & 0x02000000) != 0)) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "Messaging Notifications" + '</div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceConnect class="form-check-input me-2" type=checkbox />' + "Laiteyhteydet" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Laitteen irtikytkennät" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceHelp class="form-check-input me-2" type=checkbox />' + "Help requests" + '</label></div>';
            }

            setModalContent('xxAddAgent', "Ilmoitusasetukset", 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: ["Ei yhteyttä", "Agentti", "Intel&reg; AMT", "Agentti + 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", "Virustentorjunta ei ole aktiivinen", "Ei automaattista päivitystä", "Palomuuri ei ole aktiivinen", "Useita kysymyksiä"] },
                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>' + "Ei mitään" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span tabindex=0 title="' + "Muokkaa laiteryhmän nimeä napsauttamalla tätä" + '" 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>' + "Virta tilat" + '</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>' + "Agenttityypit" + '</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([["Ei yhteyttä", "Agentti", "Intel&reg; AMT", "Agentti + 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>' + "Liitettävyys" + '</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 /> ' + "Virustentorjunta ei ole aktiivinen",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noupdate")\' width=10 height=10 /> ' + "Ei automaattista päivitystä",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:nofirewall")\' width=10 height=10 /> ' + "Palomuuri ei ole aktiivinen",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:any")\' width=10 height=10 /> ' + "Useita kysymyksiä"
                    ][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>' + "Turvallisuus" + '</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>' + "Ei laitteita tässä laiteryhmässä" + '</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="' + "Näytä julkinen linkki" + '" onclick=\'return p5showPublicLink("' + (publicPath + '/' + encodeURIComponentEx(f.nx)) + '?download=1' + loginkey + '")\'></i>';
                        publiclink += '<i class="fa-solid fa-download fa-fw" title="' + "Kopioi linkki leikepöydälle" + '" 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 "Tallennusraja ylitetty";
            if (bytes < 2048) return format("{0} tavua jäljellä", bytes);
            if (bytes < 2097152) return format("{0} kilotavua jäljellä", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("{0} megatavua jäljellä", Math.round(bytes / 1024 / 1024));
            return format("{0} gigatavua jäljellä", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize2(bytes) {
            if (bytes <= 0) return "Ei mitään";
            if (bytes < 2048) return format("{0} b", bytes);
            if (bytes < 2097152) return format("{0} Kb", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("{0} Mb", Math.round(bytes / 1024 / 1024));
            return format("{0} Gb", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize3(bytes) {
            if (bytes <= 0) return "Ei mitään";
            if (bytes < 2048) return format("{0} b", bytes);
            if (bytes < 2097152) return format("{0} Kb", round(bytes / 1024, 1));
            if (bytes < 2147483648) return format("{0} Mb", round(bytes / 1024 / 1024, 1));
            return format("{0} Gb", round(bytes / 1024 / 1024 / 1024, 1));
        }

        function getNetworkSpeed(bitsPerSecond) {
            if (bitsPerSecond <= 0) return "0 bps";
            if (bitsPerSecond < 1000) return format("{0} bps", bitsPerSecond);
            if (bitsPerSecond < 1000000) return format("{0} Kbps", Math.round(bitsPerSecond / 1000));
            if (bitsPerSecond < 1000000000) return format("{0} Mbps", Math.round(bitsPerSecond / 1000000));
            return format("{0} Gbps", (bitsPerSecond / 1000000000).toFixed(1));
        }

        function p5getQuotabar(f) {
            while (f.t > 1 && f.t != 4) { f = f.parent; }
            if ((f.t != 1 && f.t != 4) || (f.maxbytes == null)) return '';
            var tf = Math.floor(f.s / 1024), tq = (f.maxbytes - f.s);
            var title;
            if (f.c > 1) { title = format("{0}k {1} tiedostossa.{2}k maksimi", tf, f.c, (Math.floor(f.maxbytes / 1024 / 1024))); } else { title = format("{0}k yhdessä tiedostossa.{1}k maksimi", 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', "Julkinen linkki", '<input type=text style=width:350px value="' + u + '" readonly /> <i class="fa-regular fa-copy" title="' + "Kopioi linkki leikepöydälle" + '" 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 ? "Poista valinta" : "Valitse kaikki");
            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 tavu"; return format("{0} tavua", 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', "Uusi kansio", '<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>' + "Rekursiivinen poisto" + '</label><br>' : '<input type=checkbox id=p5recdeleteinput class="form-check-input me-2" style=\'display:none\'>';
            setModalContent('xxAddAgent', "Poista", (cc > 1) ? (format("Poista {0} valittua kohdetta?", cc) + rec) : ("Poista valittu kohde?" + 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', "Nimeä uudelleen", '<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', "Lataa tiedosto", '<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\')" />' + "Haluatko varmasti korvata olemassa olevan?" + '</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("Tiedostoeditori", "Vain alle 200 kt: n tiedostoja voidaan muokata."); }
                    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("Haluatko varmasti {0} {1} kohdetta tänne?", (p5clipboardCut == 0 ? 'copy' : 'move'), p5clipboard.length, ((p5clipboard.length > 1) ? 's' : '')) }
            setModalContent('xxAddAgent', "Liitä", 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} kohde{1} valittu {2} varten", p5clipboard.length, ((p5clipboard.length > 1) ? 's' : ''), (p5clipboardCut == 0 ? "Kopioi" : "Move")) + ', <a href=# onclick="return p5clearClip()" style=cursor:pointer>' + "Tyhjennä" + '</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', "Lataa tiedosto", format((overWriteCount == 1) ? "Lataus korvaa 1 tiedoston. Jatkaa?" : "Lataus korvaa {0} tiedostoa. Jatkaa?", 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: "Tilille kirjautuminen",
            2: "Tilin uloskirjautuminen",
            3: "Kieli vaihdettu kielestä {1} kieleksi {2}",
            4: "Liittyi työpöydän multiplex-istuntoon", // No longer in use, replaced with 143
            5: "Poistu työpöydän multiplex-istunnosta", // No longer in use, replaced with 144
            6: "Aloitettu työpöydän multipleksointiistunto", // No longer in use, replaced with 145
            7: "Tallennusistunto valmis, {0} sekuntia", // No longer in use, replaced with 146
            8: "Suljettu työpöydän multiplex-istunto, {0} sekuntia", // No longer in use, replaced with 147
            9: "Välitysistunto \"{0}\" päättyi välillä {1} - {2}, {3} sekuntia",
            10: "Pääteistunto \"{0}\" päättyi välillä {1} - {2}, {3} sekuntia",
            11: "Työpöytäistunto \"{0}\" päättyi välillä {1} - {2}, {3} sekuntia",
            12: "Tiedostonhallintaistunto \"{0}\" päättyi välillä {1} - {2}, {3} sekuntia",
            13: "Aloitti välitysistunnon \"{0}\" välillä {1} - {2}",
            14: "Aloitettu pääteistunto \"{0}\" välillä {1} - {2}",
            15: "Aloitettu työpöytäistunto \"{0}\" välillä {1} - {2}",
            16: "Tiedostonhallintaistunto \"{0}\" aloitettu välillä {1} - {2}",
            17: "Käsitellään konsolikomentoa: \"{0}\"",
            18: "Näytetään viestiruutu, title = \"{0}\", message = \"{1}\"",
            19: "Tappamisprosessi {0} ({1})",
            20: "Avaaminen: {0}",
            21: "Haetaan leikepöydän sisältöä, {0} tavua",
            22: "Asetetaan leikepöydän sisältöä, {0} tavua",
            23: "Yritetään Intel (R) AMT ACM -tilan aktivointia",
            24: "Komentojen suorittaminen",
            25: "Suoritetaan tehotoimintaa = {0}, pakotettua = {1}",
            26: "Näytetään paahtoleipää, title = \"{0}\", message = \"{1}\"",
            27: "Paikallinen käyttäjä hyväksyi etäpäätepyynnön",
            28: "Paikallinen käyttäjä hylkäsi etäpäätepyynnön",
            29: "Paikallinen käyttäjä on sulkenut etätyöpöytäyhteyden voimakkaasti",
            30: "Aloitetaan etätyöpöytä sen jälkeen, kun paikallinen käyttäjä on hyväksynyt",
            31: "Etätyöpöytäyhteyspalkki aktivoitu / päivitetty",
            32: "Etätyöpöytäyhteyspalkki epäonnistui tai sitä ei tueta",
            33: "Paikallinen käyttäjä on sulkenut etätyöpöytäyhteyden voimakkaasti", // No longer used, 29 must be used instead.
            34: "Etätyöpöydän käynnistäminen epäonnistui, kun paikallinen käyttäjä hylkäsi",
            35: "Aloitettiin etätyöpöytä paahtolehden ilmoituksella",
            36: "Aloitettiin etätyöpöytä ilman ilmoitusta",
            37: "Etätyöpöytäyhteyspalkki aktivoitu / päivitetty", // No longer used, 31 must be used instead.
            38: "Etätyöpöytäyhteyspalkki epäonnistui tai sitä ei tueta", // No longer used, 33 must be used instead.
            39: "Paikallinen käyttäjä on sulkenut etätyöpöytäyhteyden voimakkaasti", // No longer used, 29 must be used instead.
            40: "Aloitetaan etätiedostot paikallisen käyttäjän hyväksynnän jälkeen",
            41: "Etätiedostojen käynnistäminen epäonnistui, kun paikallinen käyttäjä hylkäsi",
            42: "Aloitettiin etätiedostot paahtoleipää koskevalla ilmoituksella",
            43: "Aloitti etätiedostot ilman ilmoitusta",
            44: "Luo kansio: \"{0}\"",
            45: "Poista: \"{0}\"",
            46: "Poista rekursiivinen: \"{0}\", {1} elementti(ä) poistettu",
            47: "Poista: \"{0}\", {1} elementti (ä) poistettu",
            48: "Nimeä uudelleen: \"{0}\" muotoon \"{1}\"",
            49: "Lataa: \"{0}\"",
            50: "Lähetä: \"{0}\"",
            51: "Kopioi: \"{0}\" kansioon \"{1}\"",
            52: "Siirrä: \"{0}\" kohtaan \"{1}\"",
            53: "Etäkäyttäjän lukitseminen työpöydältä",
            54: "Agentin suljettu istunto, jossa {0}% agentti-palvelin-pakkaus. Lähetetty: {1}, Pakattu: {2}",
            55: "Luotu laiteryhmä: {0}",
            56: "Laiteryhmä kumottu: {0}",
            57: "Lisätty laite {0} laiteryhmään {1}",
            58: "Laite pyysi Intel (R) AMT ACM -aktivointia, FQDN: {0}",
            59: "Vaihdettu laite {0} ryhmästä {1}: {2}",
            60: "Poistettiin käyttäjän {0} käyttäjän laitteen oikeudet",
            61: "Muutti käyttäjän laitteen käyttöoikeuksia verkkotunnukselle {0}",
            62: "Poistettiin käyttäjä {0} käyttäjäryhmästä {1}",
            63: "Tili poistettu",
            64: "Tili luotu, käyttäjänimi on {0}",
            65: "Tili luotu, sähköpostiosoite on {0}",
            66: "Tili muutettu: {0}",
            67: "Käyttäjäryhmän jäsenyys muutettu: {0}",
            68: "Lisätty käyttäjäryhmä {0} laiteryhmään {1}",
            69: "Luotu käyttäjäryhmä: {0}",
            70: "Poistettiin käyttäjäryhmä {0} laiteryhmästä {1}",
            71: "Lisätty käyttäjä {0} käyttäjäryhmään {1}",
            72: "Poistettiin käyttäjä {0} käyttäjäryhmästä {1}",
            73: "Laiteryhmäilmoitus muutettu",
            74: "Tilin salasana vaihdettu: {0}",
            75: "Muutetut tilin tunnistetiedot",
            76: "Luotu laiteryhmä: {0}",
            77: "Laiteryhmä poistettu: {0}",
            78: "Laiteryhmän jäsenyys muutettu: {0}",
            79: "Muutettu käyttäjäryhmä: {0}",
            80: "Lisätty käyttäjä {0} käyttäjäryhmään {1}",
            81: "Poistettiin käyttäjän {0} käyttäjän laitteen oikeudet",
            82: "Muutti käyttäjän laitteen käyttöoikeuksia verkkotunnukselle {0}",
            83: "Poistettiin käyttäjä {0} laiteryhmästä {1}",
            84: "Lisätty laite {0} laiteryhmään {1}",
            85: "Siirretty laite {0} ryhmään {1}",
            86: "Poistettiin käyttäjän {0} käyttäjän laitteen oikeudet",
            87: "Poistettiin laite {0} laiteryhmästä {1}",
            88: "Otti käyttöön kaksivaiheisen todennuksen sähköpostilla",
            89: "Poistettu käytöstä kaksivaiheinen sähköpostitodennus",
            90: "Lisätty todennussovellus",
            91: "Poistettu todennussovellus",
            92: "Uudet 2FA-varmuuskopiokoodit luotu",
            93: "2FA-varmuuskopiokoodit tyhjennetty",
            94: "Poistettu suojausavain",
            95: "Lisätty suojausavain",
            96: "Vahvistettu käyttäjän {0} puhelinnumero",
            97: "Käyttäjän {0} puhelinnumero poistettu",
            98: "Apua pyydetään, käyttäjä: {0}, tiedot: {1}",
            99: "Komentojen suorittaminen käyttäjänä",
            100: "Suorita komennot käyttäjänä, jos mahdollista",
            101: "Laitteen jako {0} lisätty kohteesta {1} kansioon {2}",
            102: "Laitteen jakaminen poistettu {0}",
            103: "Erä lähetetään {0} tiedostoa kansioon {1}",
            104: "Agentin ydintiedoston automaattinen lataus: \"{0}\"",
            105: "Lähetys: \"{0}\", Koko: {1}",
            106: "Lataa: \"{0}\", koko: {1}",
            107: "Tilin kirjautuminen käyttäjiltä {0}, {1}, {2}",
            108: "Käyttäjän kirjautumisyritys, jonka toinen tekijä on virheellinen {0}, {1}, {2}",
            109: "Käyttäjän kirjautumisyritys lukitulle tilille: {0}, {1}, {2}",
            110: "Virheellinen käyttäjän kirjautumisyritys käyttäjiltä {0}, {1}, {2}",
            111: "Laite pyysi Intel (R) AMT ACM TLS -aktivointia, FQDN: {0}",
            112: "Messenger -istunto \"{0}\" päättyi {1} - {2}, {3} sekuntia",
            113: "Lisätty push -ilmoitusten todennuslaite",
            114: "Push -ilmoitusten todennuslaite poistettu",
            115: "Lisätty kirjautumistunnus",
            116: "Kirjautumistunnus poistettu",
            117: "Tämä on vanha agenttiversio, harkitse päivittämistä.",
            118: "Tällä agentilla on vanhentunut varmenteen vahvistusmekanismi, harkitse päivittämistä.",
            119: "Tämä agentti käyttää turvattomia tunneleita, harkitse päivittämistä.",
            120: "Alkoi paikallinen välitysistunto \"{0}\", protokolla {1} käyttäjälle {2}",
            121: "Paikallinen välitysistunto \"{0}\", protokolla {1} - {2}, {3} sekuntia",
            122: "Poistui työpöydän multipleksi-istunnosta {0} sekunnin kuluttua.", // No longer in use, replaced with 144
            123: "Poistui Web-SSH-istunnosta \"{1}\" {0} sekunnin kuluttua.",
            124: "Poistui Web-SFTP-istunnosta \"{1}\" {0} sekunnin kuluttua.",
            125: "Poistui Web-RDP-istunnosta \"{1}\" {0} sekunnin kuluttua.",
            126: "Poistui Web-VNC-istunnosta {0} sekunnin kuluttua.",
            127: "Tilin näyttönimeksi vaihdettiin {0}.",
            128: "Tili luotu, nimi on {0}.",
            129: "Tilin näyttönimi poistettu.",
            130: "Käyttäjäilmoitukset muutettu",
            131: "Lisätty laitteen jako {0} rajoittamattomaksi ajaksi.",
            132: "Kiihottua.",
            133: "Sammuttaa.",
            134: "Käyttäjän {0} yhteys pakotettu katkaistiin",
            135: "Käyttäjän {0} väkisin katkaistu pääteistunto",
            136: "Käyttäjän {0} väkisin katkaistu tiedostoistunto",
            137: "Käyttäjän {0} yhteys pakotettu katkaistu reititysistuntoon",
            138: "Lisätty laitejako {0} toistuvasti päivittäin.",
            139: "Lisätty laitejako {0} toistuvasti viikoittain.",
            140: "Vaihdettu laite {0} ryhmästä {1}: {2}",
            141: "Intel(r) AMT -käytännön muutos",
            142: "Laiteryhmää {0} muutettiin: {1}",
            143: "Liittyi työpöydän multipleksointiistuntoon \"{0}\"",
            144: "Poistui työpöydän multipleksointiistunnosta \"{0}\" {1} sekunnin kuluttua.",
            145: "Aloitettu työpöytämultipleksiistunto \"{0}\"",
            146: "Tallennusistunto \"{0}\" valmis, {1} sekuntia",
            147: "Suljettu työpöydän multipleksointiistunto \"{0}\", {1} sekuntia",
            148: "Web-SSH-istunto \"{0}\" aloitettu.",
            149: "Web-SFTP-istunto \"{0}\" aloitettu.",
            150: "Web-RDP-istunto \"{0}\" aloitettu.",
            151: "Web-VNC-istunto \"{0}\" aloitettu.", // Not in use yet
            152: "Ei enää rele kohteelle \"{0}\".",
            153: "On rele kohteelle \"{0}\".",
            154: "Account changed to sync with LDAP data.",
            155: "Denied user login from {0}, {1}, {2}",
            156: "Verified messaging account of user {0}",
            157: "Removed messaging account of user {0}",
            158: "Displaying alert box, title=\"{0}\", message=\"{1}\"",
            159: "Device Powered On",
            160: "Enabled Duo two-factor authentication",
            161: "Disabled Duo two-factor authentication",
            162: "Aloitti Messenger -istunto \"{0}\" välillä {1} - {2}",
            163: "Poistettu laite {0} käyttäjäryhmästä {1}",
            164: "Luo tiedosto: \"{0}\""
        };

        var eventsShortMessageId = {
            1: "Ryhmän nimi",
            2: "Kuvaus",
            3: "Liput",
            4: "Suostumus",
            5: "Automaattinen-Poisto",
            6: "Kutsukoodi",
            7: "Rele laite"
        }

        // 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="' + "Tämä on vieraiden jakamisistunto" + '">' + 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>' + "Ei tapahtumia" + '</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("Raporttityyppi", '<select id=d2reportType class="form-select" onchange=p3showReportValidation()><option value=1>' + "Kaikki tapahtumat" + '</option>');
            x += addHtmlFormFloating("Aikajänne", '<select id=d2reportSpan class="form-select" onchange=p3showReportValidation()><option value=1>' + "Kaikki käytettävissä olevat" + '</option><option value=2>' + "Yksi päivä" + '</option></select>');
            x += '<div id=d2daySelector>' + addHtmlFormFloating("Raporttipäivä", '<input type=date id=d2reportDay class="form-control" onchange=p3showReportValidation()></input>') + '</div>';
            x += addHtmlFormFloating("Ryhmittele", '<select id=d2reportGroupBy class="form-select" onchange=p3showReportValidation()><option value=1>' + "Ei mitään" + '</option><option value=2>' + "Laiteryhmä" + '</option><option value=3>' + "Käyttäjä" + '</option>');
            x += addHtmlFormFloating("Muoto", '<select id=d2reportFormat class="form-select" onchange=p3showReportValidation()><option value=1>' + "CSV" + '</option><option value=2>' + "JSON" + '</option>');
            x += '</div>';
            setModalContent('xxAddAgent', "Lataa raportti", 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 = "Lataa luettelo tapahtumista, jollakin alla olevista tiedostomuodoista." + '<br /><br />';
            x += addHtmlValue("CSV muoto", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogCSV(' + mode + ')">' + 'eventslist.csv' + '</a>');
            x += addHtmlValue("JSON Muoto", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogJSON(' + mode + ')">' + 'eventslist.json' + '</a>');
            setModalContent('xxAddAgent', "Tapahtumaluettelon vienti", 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, aika, tyyppi, toiminta, käyttäjä, laite, viesti" + '\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>' + "Nimi" + '<th style=width:80px>' + "Laiteryhmät" + '<th style=width:120px>' + nobreak("Viimeisin käyttö") + '<th style=width:120px>' + "Käyttöoikeudet";

            // 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>' + "Online-tilassa olevat käyttäjät"; 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>' + "Offline-tilassa olevat käyttäjät"; addHeader = false; }
                        x += addUserHtml(user, sessions);
                        maxUsers--;
                    } else {
                        hiddenUsers++;
                    }
                }
            }
            x += '</table>';
            if (hiddenUsers == 1) { x += '<br />' + "1 muuta käyttäjää ei näy, käytä hakukenttää etsiäksesi käyttäjiä ..." + '<br />'; }
            else if (hiddenUsers > 1) { x += '<br />' + format("{0} muuta käyttäjää ei näy, etsi käyttäjiä hakukentän avulla ...", hiddenUsers) + '<br />'; }
            if (maxUsers == displayUsersLimit) { x += '<br />' + "Käyttäjiä ei löytynyt." + '<br />'; }
            QH('p3users', x);

            // Re-check userid's
            elements = document.getElementsByClassName('UserCheckbox');
            var eself = encodeURIComponentEx(userinfo._id);
            for (var i = 0; i < elements.length; i++) { elements[i].checked = ((checkedUserids.indexOf(elements[i].value) >= 0) && (elements[i].value != eself)); }
            p3updateInfo();

            // Update current user panel if needed
            if ((currentUser != null) && (xxcurrentView == 30)) { gotoUser(encodeURIComponentEx(currentUser._id), true); }
        }

        function addUserHtml(user, sessions) {
            var x = '', gray = ' gray', msg = '', self = (user._id != userinfo._id), lastAccess = '', permissions = '';
            if (sessions != null) {
                gray = '';
                if (self) {
                    msg = '<span style=float:right;margin-top:1px;margin-right:4px title=' + "Chat" + '><a href=# onclick=userChat(event,"' + encodeURIComponentEx(user._id) + '","' + encodeURIComponentEx(user.name) + '")><img src=\'images/icon-chat.png\' height=16 width=16 style=padding-top:2px /></a></span>';
                    msg += '<span style=float:right;margin-top:1px;margin-left:4px;margin-right:4px title=Notify><a href=# onclick=\'return showUserAlertDialog(event,"' + encodeURIComponentEx(user._id) + '")\'><img src=\'images/icon-notify.png\' height=16 width=16 style=padding-top:2px /></a></span>';
                }
                if (sessions == 1) { lastAccess += nobreak("1 istunto"); } else { lastAccess += nobreak(format("{0} istuntoa", sessions)); }
            } else {
                if (user.access) { lastAccess += '<span title="' + format("Viimeisin käyttöoikeus: {0}", printDateTime(new Date(user.access * 1000))) + '">' + printDate(new Date(user.access * 1000)) + '</span>'; }
                else if (user.login) { lastAccess += '<span title="' + format("Edellinen kirjautuminen: {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 += "Lukittu" + ',&nbsp;'; }
            permissions += '<span title=\'' + "Palvelimen oikeudet" + '\'>';

            var urights = user.siteadmin & (0xFFFFFFFF - 1248);
            if ((user.siteadmin == null) || (urights == 0)) {
                permissions += "Käyttäjä";
            } else if (urights == 8) {
                permissions += "Käyttäjä + tiedostot";
            } else if (user.siteadmin == 0xFFFFFFFF) {
                permissions += "Järjestelmänvalvoja";
            } else if ((urights & 2) != 0) {
                permissions += "Johtaja";
            } else {
                permissions += "Osittainen";
            }
            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="' + "Sähköposti osoitetta ei ole vahvistettu" + '">&#x2717;</b>' : ' <b style=color:green title="' + "Sähköposti on vahvistettu" + '">&#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="' + "Lähetä käyttäjälle sähköposti" + '" 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="' + "kaksivaiheinen todennus käytössä" + '" style="margin-top:2px"></i>'; }
            if (user.phone != null) { username += ' <i class="fa-solid fa-mobile-screen" title="' + "Vahvistettu puhelinnumero" + '" 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="' + "Tili on lukittu" + '" style="margin-top:2px" />'; }
            if ((user.msghandle != null) && (features2 & 0x02000000)) { username += ' <i class="fa-solid fa-comment fa-xs" title="' + "Verified messaging account" + '" 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) ? "Poista valinta" : "Valitse kaikki";
        }

        // 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>' + "Vahvista sähköposti" + '</option><option value=5>' + "Mitätöi sähköpostiosoite" + '</option>'; }

            var x = "Valitse kaikille valituille käyttäjille suoritettava toiminto." + '<br /><br />';
            x += addHtmlFormFloating("Operaatio", '<select id=d3groupop class=form-select><option value=1>' + "Lukitse tili" + '</option><option value=2>' + "Poista tilin lukitus" + '</option>' + optionalActions + '<option value=3>' + "Poista tili" + '</option></select>');
            setModalContent('xxAddAgent', "Ryhmätoiminta", 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 = "Haluatko varmasti poistaa valitut tilit?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p3usersGroupActionFunctionDelCheck() />' + "Varmista" + '</label>';
                setModalContent('xxAddAgent', "Poista tilit", 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', "Lähetä tekstiviesti", '<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', "Send Message", '<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="' + "Otsikko" + '"></input>';
            x += '<textarea id=d2emailText maxlength=10000 style="width:100%;height:200px;resize:none;overflow-y:auto" onKeyUp=showSendEmailValidate()></textarea>';

            setModalContent('xxAddAgent', "Lähetä Sähköposti", 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>' + "Lähetä tekstiviesti tälle käyttäjälle." + '</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>' + "Näytä viesti, kunnes käyttäjä hylkäsi sen" + '</option>';
            x += '<option value=10>' + "Näytä 10 sekuntia" + '</option>';
            x += '<option value=60>' + "Näytä yhden minuutin ajan" + '</option>';
            x += '<option value=300>' + "Näytä 5 minuuttia" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', format("Ilmoita {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') + '>' + "Näytä vain ensimmäiset 100 käyttäjää" + '</label><br />';
            setModalContent('xxAddAgent', "Käyttäjien näkymä", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', onUsersViewSettingsEx);
        }

        function onUsersViewSettingsEx() {
            userViewSettings.noViewLimit = !Q('d2c1').checked;
            putstore('_usersViewSettings', JSON.stringify(userViewSettings));
            mainUpdate(16384); // Update users
        }

        function p4batchAccountCreate() {
            if (xxdialogMode) return;
            var x = "Create many accounts at once by importing a JSON or CSV file" + '<br /><br />';
            x += "JSON file format is as follows:" + '<br />';
            x += '<pre>[\r\n {"user":"x1","pass":"x","email":"x1@x"},\r\n {"user":"x2","pass":"x","resetNextLogin":true}\r\n]</pre><br />';
            x += "CSV file format is as follows:" + '<br />';
            x += '<pre>user,pass,email,resetNextLogin\r\nx1,x,x1@x,\r\nx2,x,,true\r\n</pre><br />';
            x += '<input type=file id=d4importFile class="form-control" accept=".json,.csv" onchange=p4batchAccountCreateValidate() />';
            setModalContent('xxAddAgent', "Käyttäjätilin tuonti", 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', "Käyttäjätilin tuonti", "Invalid CSV file format.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Käyttäjätilin tuonti", "Invalid CSV file format.");
                    }
                } else {
                    try {
                        j = JSON.parse(r.target.result);
                    } catch (ex) {
                        setModalContent('xxAddAgent', "Käyttäjätilin tuonti", format("JSON-tiedosto {0} on virheellinen.", 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', "Käyttäjätilin tuonti", "Virheellinen JSON-tiedostomuoto.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Käyttäjätilin tuonti", "Virheellinen JSON-tiedostomuoto.");
                    }
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            };
            fr.readAsText(Q('d4importFile').files[0]);
        }

        function p4downloadUserInfo() {
            if (xxdialogMode) return;
            var x = "Lataa luettelo käyttäjistä, jollakin alla olevista tiedostomuodoista." + '<br /><br />';
            x += addHtmlValue("CSV muoto", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoCSV()\'>' + 'userlist.csv' + '</a>');
            x += addHtmlValue("JSON Muoto", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoJSON()\'>' + 'userlist.json' + '</a>');
            setModalContent('xxAddAgent', "Käyttäjäluettelon vienti", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

        function p4downloadUserInfoCSV() {
            var csv = "id,nimi,sähköposti,luominen,viimeinen kirjautuminen,ryhmät,authfactors,siteadmin,useradmin,lukittu" + '\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>' + "Lähetä viesti kaikille yhteydessä oleville käyttäjille." + '</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>' + "Näytä viesti, kunnes käyttäjä hylkäsi sen" + '</option>';
            x += '<option value=10>' + "Näytä 10 sekuntia" + '</option>';
            x += '<option value=60>' + "Näytä yhden minuutin ajan" + '</option>';
            x += '<option value=300>' + "Näytä 5 minuuttia" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', "Lähetä viesti", 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] == '') ? "Oletus" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Verkkotunnus", y);
            }

            if ((features & 0x200000) == 0) { x += addHtmlFormFloating('<span id=p4hname>' + "Käyttäjätunnus" + '</span>', '<input id=p4name class="form-control" maxlength=64 autocomplete=username onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />'); }
            x += addHtmlFormFloating('<span id=p4hemail>' + "Sähköposti", '<input id=p4email type=email class="form-control" maxlength=256 autocomplete="email" inputmode="email" onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />');
            x += '';
            x += addHtmlFormFloating('<span id=p4hp1>' + "Salasana" + '</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" />' + "Satunnaista salasana." + '</label></div>';
            x += '<div><label><input id=p4removeEvents onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Poista kaikki tämän käyttäjätunnuksen aiemmat tapahtumat." + '</label></div>';
            x += '<div><label><input id=p4resetNextLogin onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Pakota salasanan palautus seuraavan kirjautumisen yhteydessä." + '</label></div>';
            if (serverinfo.emailcheck) {
                x += '<div><label><input id=p4verifiedEmail onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Sähköposti osoite on vahvistettu." + '</label></div>';
                x += '<div><label><input id=p4invitationEmail type=checkbox class="form-check-input me-2" title=' + "Sähköposti osoite vahvistettu ja salasanan palautus pakotettu." + ' />' + "Lähetä kutsusähköposti." + '</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("Vaatimukset: {0}.", r.join(', ')) + '</div>'; }
            }

            setModalContent('xxAddAgent', "Luo käyttäjätunnus", 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 = "Syötä pilkulla eroteltu luettelo hallinnollisten alueiden nimistä." + '<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', "Hallinnollinen alue", 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>' + "Palvelimen Tiedostot" + '</label>, <input type=number class="me-1 form-control" onchange=showUserAdminDialogValidate() maxlength=10 id=ua_fileaccessquota>' + "k max, tyhjä oletuksena" + '</div><br><hr/>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_fulladmin>' + "Täysi Järjestelmänvalvoja" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverbackup>' + "Palvelimen varmuuskopiointi" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverrestore>' + "Palvelimen Palautus" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverupdate>' + "Palvelimen päivitykset" + '</label><br></div>';
            x += '<div id=d2MngUsr><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusers>' + "Hallitse käyttäjiä" + '</label><br></div>';
            x += '<div id=d2MngGrp><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusergroups>' + "Hallitse käyttäjäryhmiä" + '</label><br></div>';
            x += '<div id=d2MngRec><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_managerecordings>' + "Hallinnoi nauhoituksia" + '</label><br></div>';
            x += '<div id=d2AllEvnt><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_allevents>' + "Näytä kaikki tapahtumat" + '</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>' + "Lukitse Tili" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewgroups>' + "Ei uusia laiteryhmiä." + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewdevices>' + "Ei uusia laitteita" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nomeshcmd>' + "Estä työkalut (MeshCmd/Router)" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_locksettings>' + "Lukitse tilin asetukset" + '</label><br>';
            x += '</div>';

            setModalContent('xxAddAgent', "Palvelimen oikeudet", 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 />' + "Ryhmiä ei löytynyt." + '<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>' + "Nimi" + '<th style=width:80px>' + "Käyttäjät" + '<th style=width:80px>' + "Laiteryhmät" + '<th style=width:80px>' + "Laitteet";
                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) ? "Poista valinta" : "Valitse kaikki";
        }

        // 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 = "Valitse kaikille valituille käyttäjille suoritettava toiminto." + '<br /><br />';
            x += addHtmlFormFloating("Operaatio", '<select id=d50groupop class=form-select><option value=1>' + "Poista ryhmä" + '</option></select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Ryhmätoiminta", 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 = "Haluatko varmasti poistaa valitut käyttäjäryhmät?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p50usersGroupActionFunctionDelCheck() />' + "Varmista" + '</label>';
                setModalContent('xxAddAgent', "Poista käyttäjäryhmät", 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("Käyttäjäryhmä", '<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] == '') ? "Oletus" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Verkkotunnus", y);
            }
            x += addHtmlFormFloating("Nimi", '<input id=p4name class=form-control maxlength=64 onchange=showCreateUserGroupDialogValidate() onkeyup=showCreateUserGroupDialogValidate() />');
            x += addHtmlFormFloating("Kuvaus", '<textarea id=p4desc class=form-control value="" maxlength=1024 /></textarea>');
            setModalContent('xxAddAgent', (mode == 1) ? "Luo käyttäjäryhmä" : "Monista käyttäjäryhmä", 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>' + "Ei mitään" + '</i>'; }
            if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { gname = '<span role=button tabindex=0 title="' + "Napsauta tätä muokataksesi käyttäjäryhmän nimeä" + '" 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>' + "Ei mitään" + '<i>'; } else { desc = EscapeHtml(desc); }

            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Nimi", 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("Verkkotunnus", (d != '') ? EscapeHtml(d) : ('<i>' + "Oletus" + '</i>'));
                x += addDeviceAttribute("Ryhmän tunniste", EscapeHtml(group._id));
            }
            if (currentUserGroup.membershipType != null) {
                x += addDeviceAttribute("Group Type", EscapeHtml(currentUserGroup.membershipType));
            }
            if ((userinfo.siteadmin & 256) != 0) {
                x += addDeviceAttribute("Kuvaus", '<span role=button onclick=p51editgroup(2,' + (currentUserGroup.membershipType != null) + ')>' + desc + ' <i class="fa-solid fa-pencil fa-xs"/></i></span>');
            } else {
                x += addDeviceAttribute("Kuvaus", desc);
            }

            // Display features
            if (serverinfo.userGroupsSessionRecording == 1) {
                var userGroupFeatures = [];
                if ((group.flags) && (group.flags & 2)) { userGroupFeatures.push("Tallenna istunnot"); }
                userGroupFeatures = userGroupFeatures.join(', ');
                if (userGroupFeatures == '') { userGroupFeatures = '<i>' + "Ei mitään" + '</i>'; }
                x += addDeviceAttribute("Ominaisuudet", 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("Työpöydän kehote+työkalurivi"); } else if (consent & 0x0040) { consentOptionsStr.push("Työpöydän työkalurivi"); } else if (consent & 0x0008) { consentOptionsStr.push("Työpöydän kehote"); } else { if (consent & 0x0001) { consentOptionsStr.push("Työpöytä ilmoitus"); } }
                if (consent & 0x0010) { consentOptionsStr.push("Terminaalikehote"); } else { if (consent & 0x0002) { consentOptionsStr.push("Ilmoita terminaalista"); } }
                if (consent & 0x0020) { consentOptionsStr.push("Tiedostot kysy"); } else { if (consent & 0x0004) { consentOptionsStr.push("Tiedostot Ilmoita"); } }
                if (consent == 7) { consentOptionsStr = ["Ilmoita aina"]; }
                if ((consent & 56) == 56) { consentOptionsStr = ["Kysy Aina"]; }

                consentOptionsStr = consentOptionsStr.join(', ');
                if (consentOptionsStr == '') { consentOptionsStr = '<i>' + "Ei mitään" + '</i>'; }
                x += addDeviceAttribute("Käyttäjän suostumus", addLinkConditional(consentOptionsStr, 'p20editmeshconsent(4)', true));
            }

            x += addDeviceAttribute("Käyttäjät", usercount);
            x += addDeviceAttribute("Laiteryhmät", meshcount);
            x += addDeviceAttribute("Laitteet", devicecount);

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

            if ((userinfo.siteadmin & 256) != 0) {
                x += '<input type=button class="btn btn-primary btn-sm" value="' + "Yleislähetys" + '" title="' + "Lähetä ilmoitus kaikille tämän ryhmän jäsenille." + '" 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> ' + "Lisää Käyttäjiä" + '</button>';
            }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Ryhmän jäsenet" + '</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="' + "Posta käyttäjältä oikeudet tähän laiteryhmään" + '" 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="' + "Käyttäjä" + '"></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>' + "Ei jäseniä" + '</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> ' + "Lisää laiteryhmä" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Yleiset laiteryhmät" + '</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>' + "Tuntematon laiteryhmä" + '</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="' + "Poista käyttäjäryhmän oikeudet tähän laiteryhmään" + '" 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="' + "Laiteryhmä" + '" 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>' + "Ei yhteisiä laiteryhmiä" + '</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> ' + "Lisää Laite" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Yleiset laitteet" + '</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>' + "Tuntematon laite" + '</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="' + "Poista tämän laitteen käyttäjäryhmien oikeudet" + '" 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="' + "Laiteryhmä" + '" 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>' + "Ei yhteisiä laitteita" + '</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>' + "Poista käyttäjäryhmä" + '</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' : '') + '>' + "Tallenna istunnot" + '</label><br></div>';
            }
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Muokkaa käyttäjäryhmän ominaisuuksia", 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', "Poista laitteen käyttöoikeudet", format("Haluatko varmasti poistaa laitteen \"{0}\" käyttöoikeudet?", 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', "Poista laiteryhmän oikeudet", format("Haluatko varmasti poistaa laiteryhmän \"{0}\" käyttöoikeudet?", 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("Nimi", '<input id=dp51name class="form-control" maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) ' + (nameReadOnly ? 'readonly disabled' : '') + '/>');
            x += addHtmlFormFloating("Kuvaus", '<textarea id=dp51desc class="form-control" maxlength=1024 style=width:100%;resize:none></textarea>');

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Muokkaa käyttäjäryhmää", 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("Poistetaanko käyttäjäryhmä {0}?", EscapeHtml(currentUserGroup.name)) + '<br /><br />';
            x += '<label><input id=p51check type=checkbox class="form-check-input me-2" onchange=p51validateDeleteGroupDialog() />' + "Varmista" + '</label>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Poista käyttäjäryhmä", 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', "Poista käyttäjän jäsenyys", format("Haluatko varmasti poistaa käytäjän \"{0}\" jäsenyyden?", EscapeHtml(decodeURIComponent(uname))));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => { meshserver.send({ action: 'removeuserfromusergroup', ugrpid: currentUserGroup._id, userid: userid }); });
        }

        function p51showAddUserDialog() {
            if (xxdialogMode) return false;
            var x = "Salli käyttäjien hallita tätä laiteryhmää ja tämän ryhmän laitteita.";
            if (features & 0x00080000) { x += " Käyttäjien on kirjauduttava sisään tähän palvelimeen kerran, ennen kuin heidät voidaan lisätä laiteryhmään." }
            x += '<br /><br /><div style=\'position:relative\'>';
            x += addHtmlFormFloating("Käyttäjätunnukset", '<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', "Lisää käyttäjät käyttäjäryhmään", 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="' + "Tili on lukittu" + '" style="margin-top:2px" /> '; msg.push("Lukittu tili"); }
            if ((user.siteadmin == null) || ((user.siteadmin & (0xFFFFFFFF - 1248)) == 0)) { msg.push("Ei palvelin oikeuksia"); } else if (user.siteadmin == 8) { msg.push("Pääsy palvelintiedostoihin"); } else if (user.siteadmin == 0xFFFFFFFF) { msg.push("Täysi järjestelmänvalvoja"); } else { msg.push("Osittaiset oikeudet"); }
            if ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & (64 + 128 + 1024)) != 0)) { msg.push("Rajoitukset"); }

            // Show user attributes
            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Nimi", user.name); } // If title bar is hidden, display the user name here
            var email = user.email ? EscapeHtml(user.email) : '<i>' + "Ei asetettu" + '</i>', everify = '';
            var realname = user.realname ? EscapeHtml(user.realname) : '<i>' + "Ei asetettu" + '</i>';
            if (serverinfo.emailcheck) { everify = ((user.emailVerified == true) ? '<b style=color:green;cursor:pointer title="' + "Sähköposti on vahvistettu" + '">&#x2713</b> ' : '<b style=color:red;cursor:pointer title="' + "Sähköposti osoitetta ei ole vahvistettu" + '">&#x2717;</b> '); }

            if ((serverinfo.crossDomain) || (debugmode != 0)) {
                var d = user._id.split('/')[1];
                x += addDeviceAttribute("Verkkotunnus", ((d != '') ? EscapeHtml(d) : ('<i>' + "Oletus" + '</i>')));
                x += addDeviceAttribute("Käyttäjätunnus", EscapeHtml(user._id));
            } else {
                if (user.name.toLowerCase() != user._id.split('/')[2]) { x += addDeviceAttribute("Käyttäjätunnus", 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("Sähköposti", '<span role=button onclick=p30showUserEmailChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + everify + email + emailLink + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
                x += addDeviceAttribute("Oikea nimi", '<span role=button onclick=p30showUserRealNameChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + realname + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            } else {
                x += addDeviceAttribute("Sähköposti", everify + email + emailLink);
                x += addDeviceAttribute("Oikea nimi", realname);
            }

            if ((features & 0x02000000) || (user.phone != null)) { // If SMS is enabled on the server or user has a phone number
                x += addDeviceAttribute("Puhelinnumero", '<span role=button onclick=p30editPhone()>' + (user.phone ? user.phone : ('<i>' + "Ei mitään" + '</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("Messaging", '<span role=button onclick=p30editMessaging()><i class="fa-solid fa-comment fa-xs" title="' + "Messaging enabled" + '" style="margin-top:2px"></i> ' + (user.msghandle ? user.msghandle : ('<i>' + "Ei mitään" + '</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("Tallenna istunnot"); }
            if (user.removeRights) {
                if ((user.removeRights & 0x00000008) != 0) { userFeatures.push("Ei kaukosäädintä"); } else {
                    if ((user.removeRights & 0x00010000) != 0) { userFeatures.push("Ei työpöytää"); }
                    else if ((user.removeRights & 0x00000100) != 0) { userFeatures.push("Vain työpöytänäkymä"); }
                    if ((user.removeRights & 0x00000200) != 0) { userFeatures.push("Ei päätettä"); }
                    if ((user.removeRights & 0x00000400) != 0) { userFeatures.push("Ei tiedostoja"); }
                }
                if ((user.removeRights & 0x00000010) != 0) { userFeatures.push("Ei konsolia"); }
                if ((user.removeRights & 0x00008000) != 0) { userFeatures.push("Ei asennusta"); }
                if ((user.removeRights & 0x00020000) != 0) { userFeatures.push("Ei kauko-ohjausta"); }
                if ((user.removeRights & 0x00000040) != 0) { userFeatures.push("Ei herätystä"); }
                if ((user.removeRights & 0x00040000) != 0) { userFeatures.push("Ei nollausta/pois"); }
            }
            userFeatures = userFeatures.join(', ');
            if (userFeatures == '') { userFeatures = '<i>' + "Ei mitään" + '</i>'; }
            x += addDeviceAttribute("Ominaisuudet", addLink(userFeatures, 'p20edituserfeatures()'));

            x += addDeviceAttribute("Palvelimen Oikeudet", '<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("Palvelimen kiintiö", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
            x += addDeviceAttribute("Luotu", printDateTime(new Date(user.creation * 1000)));
            if (user.login) x += addDeviceAttribute("Edellinen kirjautuminen", printDateTime(new Date(user.login * 1000)));
            if (user.passchange == -1) { x += addDeviceAttribute("Salasana", "Vaihdetaan seuraavan kirjautumisen yhteydessä"); }
            else if (user.passchange) { x += addDeviceAttribute("Salasana", format("Muutettu viimeksi: {0}", printDateTime(new Date(user.passchange * 1000)))); }

            // Device Groups
            var linkCount = 0, linkCountStr = '<i>' + "Ei mitään" + '<i>';
            if (user.links) {
                for (var i in user.links) { if (i.startsWith('mesh/')) { linkCount++; } }
                if (linkCount == 1) { linkCountStr = "1 ryhmä"; } else if (linkCount > 1) { linkCountStr = format("{0} ryhmää", linkCount); }
            }
            x += addDeviceAttribute("Laiteryhmät", linkCountStr);

            // Administrative Realms
            if ((userinfo.siteadmin == 0xFFFFFFFF) || (userinfo.siteadmin & 2)) {
                var xuserGroups = '<i>' + "Ei mitään" + '</i>';
                if (user.groups) { xuserGroups = ''; for (var i in user.groups) { xuserGroups += '<span class="tagSpan">' + EscapeHtml(user.groups[i]) + '</span>'; } }
                x += addDeviceAttribute("Pääkäyttäjän alueet", 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("Työpöydän kehote+työkalurivi"); } else if (consent & 0x0040) { meshFeatures.push("Työpöydän työkalurivi"); } else if (consent & 0x0008) { meshFeatures.push("Työpöydän kehote"); } else { if (consent & 0x0001) { meshFeatures.push("Työpöytä ilmoitus"); } }
                if (consent & 0x0010) { meshFeatures.push("Terminaalikehote"); } else { if (consent & 0x0002) { meshFeatures.push("Ilmoita terminaalista"); } }
                if (consent & 0x0020) { meshFeatures.push("Tiedostot kysy"); } else { if (consent & 0x0004) { meshFeatures.push("Tiedostot Ilmoita"); } }
                if (consent == 7) { meshFeatures = ["Ilmoita aina"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Kysy Aina"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Ei mitään" + '</i>'; }
                x += addDeviceAttribute("Käyttäjän suostumus", 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("Todennussovellus"); }
                if (user.otphkeys > 0) { factors.push("Suojausavain"); }
                if (user.otpekey > 0) { factors.push("Sähköposti"); }
                if (user.otpduo > 0) { factors.push("Duo"); }
                if (user.otpkeys > 0) { factors.push("Varmuuskoodit"); }
                if (user.otpdev > 0) { factors.push("Laitteen työntö"); }
                if ((user.phone != null) && (features & 0x04000000)) { factors.push("Tekstiviesti"); }
                if ((user.msghandle != null) && (features2 & 0x04000000)) { factors.push("Messaging"); }
                x += addDeviceAttribute("Turvallisuus", '<i class="fa-solid fa-key" title="' + "kaksivaiheinen todennus käytössä" + '" 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="' + "Muistiinpanot" + '" title="' + "Näytä tämän Käyttäjän huomautukset" + '" onclick=showNotes(false,"' + encodeURIComponentEx(user._id) + '") />';
            if (user.phone && (features & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Tekstiviesti" + '" title="' + "Lähetä tekstiviesti tälle käyttäjälle" + '" onclick=showSendSMS("' + encodeURIComponentEx(user._id) + '") />'; }
            if (user.msghandle && (features2 & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Viesti" + '" title="' + "Send a message to this user" + '" 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="' + "Sähköposti" + '" title="' + "Lähetä sähköposti tälle käyttäjälle" + '" 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="' + "Ilmoita" + '" title="' + "Lähetä käyttäjän ilmoitus" + '" onclick=showUserAlertDialog(event,"' + encodeURIComponentEx(user._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Chat" + '" title="' + "Chat" + '" onclick=userChat(event,"' + encodeURIComponentEx(user._id) + '","' + encodeURIComponentEx(user.name) + '") />';
                if ((activeSessions > 0) && (serverinfo != null) && (serverinfo.altmessenging != null)) {
                    for (var i in serverinfo.altmessenging) {
                        var am = serverinfo.altmessenging[i];
                        if ((am.type == null) || (am.type == 'user')) {
                            x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + EscapeHtml(am.name) + '" onclick=altUserChat(event,"' + encodeURIComponentEx(user._id) + '","' + encodeURIComponentEx(user.name) + '",' + i + ') />';
                        }
                    }
                }
            }

            // Setup the panel
            QH('p30html', x);

            // Draw the user timeline
            drawUserPermissions();

            // Check if we can change password / delete this user
            var userAdminRights = (((userinfo.siteadmin != null) && (userinfo.siteadmin & 2) && (user.siteadmin != 0xFFFFFFFF)) || (userinfo.siteadmin == 0xFFFFFFFF));

            // Show bottom buttons
            x = '<div style=float:right;font-size:small>';
            if (userAdminRights && (userinfo._id != user._id)) { x += '<a href=# style=cursor:pointer onclick=\'return p30showDeleteUserDialog()\' title="' + "Poista tämä käyttäjä" + '">' + "Poista Käyttäjä" + '</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="' + "Vaihda tämän käyttäjän salasana" + '">' + "Vaihda Salasana" + '</a>';
                x += ' <a href=# style=cursor:pointer onclick=\'return p30viewPreviousLogins()\' title="' + "Näytä tämän käyttäjän aiemmat kirjautumistiedot" + '">' + "Edelliset kirjautumiset" + '</a>';
            }
            x += '</div><br>'
            QH('p30html3', x);

            // Update user's connection state
            x = '';
            if (activeSessions == 1) { x = "1 aktiivinen istunto"; } else if (activeSessions > 1) { x = format("{0} aktiivista istuntoa", 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-yhteensopiva puhelinnumero tälle käyttäjälle." + '<br />' + "Jätä tyhjäksi.";
            x += '<br /><br /><div style=width:100%;text-align:center>' + "Puhelinnumero:" + ' <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', "Puhelin ilmoitukset", 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%>' + "Messaging account for this user.";
            var y = '<select id=d2serviceselect style=width:160px;margin-left:8px onchange=p30editMessagingValidate()><option value=0>' + "Ei mitään" + '</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>' + "Service" + '<td>' + y;
            x += '<tr><td>' + "Handle" + '<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">' + "Join this Discord server to receive notifications." + '</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">' + "Information at Pushover.net" + '</a></div>';
            x += '<div id=d2ntfyinfo style=display:none><br /><a href="' + (serverinfo.userMsgNftyUrl ? serverinfo.userMsgNftyUrl : 'https://ntfy.sh/') + '" target="_ntfy">' + "Free service at ntfy.sh" + '</a></div>';
            x += '<div id=d2slackinfo style=display:none><br /><a href=https://api.slack.com/messaging/webhooks target="_slack">' + "Slack Webhook Setup" + '</a></div>';
            xxdialogButtons = 3; xxdialogTag = 'verifyMessaging';
            setModalContent('xxAddAgent', "Messaging Notifications", 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'] = "username@server.com"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "User key"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Aihe"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "username@sample.com"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Käyttäjätunnus"; }
            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' : '') + '>' + "Tallenna istunnot" + '</label><br></div>';
            }
            x += '<div><label><input type=checkbox id=d20flag7 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000008) ? 'checked' : '') + '>' + "Ei kaukosäädintä" + '</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' : '') + '>' + "Ei työpöytä yhteyttä" + '</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' : '') + '>' + "Vain etäkatselu" + '</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' : '') + '>' + "Ei pääsyä terminaaliin" + '</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' : '') + '>' + "Ei tiedostojen käyttöä" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag6 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000010) ? 'checked' : '') + '>' + "Ei agenttikonsolia" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag8 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00008000) ? 'checked' : '') + '>' + "Ei asennusta" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag9 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00020000) ? 'checked' : '') + '>' + "Ei kauko-ohjausta" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag10 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000040) ? 'checked' : '') + '>' + "Ei herätystä" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag11 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00040000) ? 'checked' : '') + '>' + "Ei nollausta/pois" + '</label><br></div>';

            setModalContent('xxAddAgent', "Muokkaa käyttäjäominaisuuksia", 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("Vaihda Nimi käyttäjälle {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("Tila", '<select id=dp30verified class="form-control" onchange=p30validateEmail()><option value=0>' + "Ei vahvistettu" + '</option><option value=1>' + "Vahvistettu" + '</option></select>'); }

            setModalContent('xxAddAgent', format("Vaihda sähköpostiosoite käyttäjälle {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', "Edelliset kirjautumiset", '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("Vaatimukset: {0}.", r.join(', ')) + '</div>'; }
            }

            x += '<div><label><input id=p4resetNextLogin class="form-check-input me-2" type=checkbox />' + "Pakota salasanan palautus seuraavan kirjautumisen yhteydessä." + '</label></div>';
            if (multiFactor == 1) { x += '<div><label><input id=p4twoFactorRemove class="form-check-input me-2" type=checkbox />' + "Poista kaikki toisen tekijän todennus." + '</label></div>'; }
            setModalContent('xxAddAgent', format("Vaihda Salasana käyttäjälle {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("Poista Käyttäjä {0}", EscapeHtml(currentUser.name)), format("Haluatko varmasti poistaa käyttäjän {0}?", EscapeHtml(currentUser.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Varmista" + '</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> ' + "Lisää laiteryhmä" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Yleiset laiteryhmät" + '</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>' + "Tuntematon laiteryhmä" + '</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="' + "Posta käyttäjältä oikeudet tähän laiteryhmään" + '" 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="' + "Laiteryhmä" + '" 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>' + "Ei yhteisiä laiteryhmiä" + '</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> ' + "Lisää Käyttäjä Ryhmä" + '</button>'; }
                }
                x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Käyttäjäryhmä jäsenyydet" + '</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>' + "Tuntematon käyttäjäryhmä" + '</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="' + "Poista käyttäjä ryhmästä" + '" 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="' + "Käyttäjäryhmä" + '" 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>' + "Ei käyttäjäryhmien jäsenyyksiä" + '</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> ' + "Lisää Laite" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Yleiset laitteet" + '</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>' + "Tuntematon laite" + '</i>');
                    if ((cr & 2) != 0) {
                        trash = '<a href=# onclick=\'return p30removeNodeFromUser(event,"' + encodeURIComponentEx(node._id) + '")\' title="' + "Posta käyttäjältä oikeudet tähän laiteryhmään" + '" 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="' + "Laite" + '" 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>' + "Ei yhteisiä laitteita" + '</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', "Poista Device Share", format("Vahvistetaanko laitteen jakamisen \"{0}\" poisto?", 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', "Poista laitteen käyttöoikeudet", format("Haluatko varmasti poistaa laitteen \"{0}\" käyttöoikeudet?", 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', "Poista käyttäjän oikeudet", name ? format("Haluatko varmasti poistaa käyttäjän \"{0}\" käyttöoikeudet?", name) : "Haluatko varmasti poistaa käyttöoikeudet?");
                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', "Poista käyttäjäryhmien käyttöoikeudet", name ? format("Haluatko varmasti poistaa käyttäjäryhmän \"{0}\" käyttöoikeudet?", name) : "Haluatko varmasti poistaa käyttöoikeudet?");
                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>' + "Tuntematon" + '</i>');
            setModalContent('xxAddAgent', "Poista käyttäjä ryhmästä", format("Haluatko varmasti poistaa käyttäjäryhmän \"{0}\" jäsenyyden?", 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("Käyttäjäryhmä", '<select id=dp2groupid class="form-select">' + y + '</select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Lisää jäsenyys", 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', "Poista laiteryhmän oikeudet", format("Haluatko varmasti poistaa laiteryhmän \"{0}\" käyttöoikeudet?", 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 ? "Vaihda valotilaa" : "Tumma tila päälle/pois";
                    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="' + "Tämä on vieraiden jakamisistunto" + '">' + 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="' + "Tämä on vieraiden jakamisistunto" + '">' + 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>' + "Ei tapahtumia" + '</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>' + "Ladataan..." + '</i></div>';
            } else if (typeof p52recordings == 'number') {
                if (p52recordings == 1) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Server is unable to read from the recordings folder." + '</i></div>'; }
                else if (p52recordings == 2) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Server is unable to get recordings from the database." + '</i></div>'; }
                else { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "An unknown error occured." + '</i></div>'; }
            } else if (p52recordings.length == 0) {
                x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Ei tallenteita." + '</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>' + "Istunto" + '</th><th style=width:110px>' + nobreak("Käynnistys aika") + '</th><th style=width:110px>' + "Kesto" + '</th><th style=width:110px>' + "Koko" + '</th></tr></thead><tbody>';
                if (p52recordings != null) {
                    var recdate = null;
                    for (var i in p52recordings) {
                        var rec = p52recordings[i], rect = new Date(rec.startTime), day = printDate(rect);
                        if (day != recdate) { recdate = day; x += '<tr><td class=userTableHeader colspan=4>' + day; }
                        x += addRecordingHtml(i, rec);
                    }
                }
                x += '</tbody></table>';
            }
            QH('p52recordings', x);

            // Re-check recordings
            elements = document.getElementsByClassName('RecordingCheckbox');
            for (var i = 0; i < elements.length; i++) { elements[i].checked = ((checkedRecordings.indexOf(elements[i].value) >= 0)); }
            p52updateInfo();
        }

        function addRecordingHtml(i, rec) {
            var sessionLengthStr = '';
            if (rec.lengthTime) { sessionLengthStr = pad2(Math.floor(rec.lengthTime / 3600)) + ':' + pad2(Math.floor((rec.lengthTime % 3600) / 60)) + ':' + pad2(Math.floor(rec.lengthTime % 60)); }
            var sessionStartStr = printTime(new Date(rec.startTime));
            if (rec.sessionStart) { sessionStartStr = printTime(new Date(rec.sessionStart)); }
            var sessionSize = '';
            if (rec.size) { sessionSize = format("{0} Kb", Math.round(rec.size / 1024)); }
            var sessionName = '<i>' + "Tuntematon" + '</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 += ' - ' + "Pääteistunto"; }
            if (rec.protocol == 2) { sessionName += ' - ' + "Työpöytäistunto"; }
            if (rec.protocol == 5) { sessionName += ' - ' + "Tiedostonsiirto"; }
            if (rec.protocol == 6) { sessionName += ' - ' + "Järjestelmänvalvojan PowerShell"; }
            if (rec.protocol == 8) { sessionName += ' - ' + "Käyttäjän kuori"; }
            if (rec.protocol == 9) { sessionName += ' - ' + "Käyttäjä PowerShell"; }
            if (rec.protocol == 100) { sessionName += ' - ' + "Intel&reg; AMT WSMAN"; }
            if (rec.protocol == 101) { sessionName += ' - ' + "Intel&reg; AMT Uudelleenohjaus"; }
            if (rec.protocol == 200) { sessionName += ' - ' + "sanansaattaja"; }

            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 = "Tuntematon";
                if (rec.protocol == 1) { protocolStr = "Pääte"; }
                if (rec.protocol == 2) { protocolStr = "Työpöytä"; }
                if (rec.protocol == 5) { protocolStr = "Tiedostot"; }
                if (rec.protocol == 6) { protocolStr = "Järjestelmänvalvojan PowerShell"; }
                if (rec.protocol == 8) { protocolStr = "Käyttäjän kuori"; }
                if (rec.protocol == 9) { protocolStr = "Käyttäjä PowerShell"; }
                if (rec.protocol == 100) { protocolStr = "Intel&reg; AMT WSMAN"; }
                if (rec.protocol == 101) { protocolStr = "Intel&reg; AMT Uudelleenohjaus"; }
                if (rec.protocol == 200) { protocolStr = "sanansaattaja"; }
                x += addHtmlValue4("Protokolla", protocolStr);
            }
            x += addHtmlValue4("Tila", (rec.present == 1) ? "Läsnä palvelimella" : "Ei palvelimella");
            if (rec.name) { x += addHtmlValue4("Laitteen Nimi", EscapeHtml(rec.name)); }
            if (rec.meshname) { x += addHtmlValue4("Laiteryhmä", EscapeHtml(rec.meshname)); }
            if (rec.size) { x += addHtmlValue4("Koko", format("{0} tavua", rec.size)); }
            if (rec.startTime) { x += addHtmlValue4("Käynnistys aika", printTime(new Date(rec.startTime))); }
            if (rec.startTime && rec.lengthTime) { x += addHtmlValue4("päättymisaika", printTime(new Date(rec.startTime + (rec.lengthTime * 1000)))); }
            if (rec.lengthTime) { x += addHtmlValue4("Kesto", 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("Multipleksori", "Käytössä"); }
            if (rec.userids) { for (var i in rec.userids) { x += addHtmlValue4("Käyttäjä", rec.userids[i].split('/')[2]); } }

            xxdialogButtons = 9;
            setModalContent('xxAddAgent', "Tallennustiedot", 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: "Etäistunnot", 2: "Käyttäjäliikenteen käyttö", 3: "Käyttäjien kirjautumiset" }
            if (userinfo.siteadmin == 0xFFFFFFFF) { options[4] = "Database Records"; }
            for (var i in options) { y += '<option value=' + i + ((settings.type == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Tyyppi", '<select id=d2reportType class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');

            x += '<div id=d2GroupByDiv style=display:none>';
            y = '';
            var options = { 1: "Käyttäjä", 2: "Laite", 3: "Päivä" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Ryhmittele", '<select id=d2groupBy class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2GroupByDiv2 style=display:none>';
            y = '';
            var options = { 1: "Käyttäjä", 3: "Päivä" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy2 == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Ryhmittele", '<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' : '') + '>' + "Kaikki" + '</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("Laiteryhmä", '<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: "Viimeinen päivä", 7: "Viimeiset 7 päivää", 30: "Viimeiset 30 päivää", 0: "Aikahaarukka" }
            for (var i in options) { y += '<option value=' + i + ((settings.timeRange == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Aika", '<select id=d2timeRange class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2timeRangeDiv style=display:none>';
            x += addHtmlFormFloating("Aika -alue alkaa", '<input id=d2timeRangeStartSelector class="flatpickr form-control" type="text" placeholder="' + "Valitse päivämäärä ja aika..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Aikaväli", '<input id=d2timeRangeEndSelector class="flatpickr form-control" type="text" placeholder="' + "Valitse päivämäärä ja aika..." + '" 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' : '') + '>' + "Näytä liikenne" + '</label><div>');
            x += '</div>';

            setModalContent('xxAddAgent', "Luo raportti", 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: "Aika", device: "Laite", session: "Istunto", user: "Käyttäjä", devgroup: "Laiteryhmä", guest: "Vieras", length: "Pituus", bytesin: "Tavua sisään", bytesout: "Bytes Out", os: "Käyttöjärjestelmä", ip: "IP-osoite", browser: "Selain", msg: "Viesti", twofactor: "2. tekijä" }
            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>' + "Raportti ei palauttanut kokonaisia." + '</tr></td>';
            } else {
                reportCSV = '"' + "Ryhmä" + '"';
                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 "Pääte";
                if (v == 2) return "Työpöytä";
                if (v == 5) return "Tiedostot";
                if (v == 6) return "Järjestelmänvalvojan PowerShell";
                if (v == 8) return "Käyttäjän kuori";
                if (v == 9) return "Käyttäjä PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "sanansaattaja";
                if (v == 201) return "Verkko-RDP";
                if (v == 202) return "Web-SSH";
                if (v == 203) return "Web-SFTP";
                if (v == 204) return "Web-VNC";
                return "Tuntematon" + ' (' + 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>' + "Tuntematon" + '</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>' + "Tuntematon" + '</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>' + "Tuntematon" + '</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>' + "Tuntematon" + '</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>' + "Tuntematon käyttäjä" + '</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>' + "Onnistunut kirjautuminen";
                if (v == 108) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny2"></div>' + "Väärä 2. tekijä";
                if (v == 109) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny4"></div>' + "Lukittu tili";
                if (v == 110) return '<div style=float:left;margin-right:4px;cursor:pointer><i class="fa-solid fa-exclamation-triangle text-warning"></i></div>' + "Virheellinen kirjautumisyritys";
            }
            if (f == '2fa') {
                if (v == '') { return "Ei mitään"; }
                if (v == 'backup') { return "Varmuuskoodit"; }
                if (v == 'fido') { return "FIDO avain"; }
                if (v == 'sms') { return "SMS viesti"; }
                if (v == 'hwotp') { return "Laitteiston OTP"; }
                if (v == 'messenger') { return "Messenging"; }
                if (v == 'push') { return "Push-ilmoitus"; }
                if (v == 'otp') { return "Kertakäyttöinen salasana"; }
                if (v == 'cookie') { return "Muista laite"; }
                if (v == 'tokenlogin') { return "Kirjautumistunnus"; }
                if (v == 'ipaddr') { return "IP-osoite"; }
                if (v == 'sso') { return "Kertakirjautuminen"; }
            }
            if (f == 'records') {
                var c = {
                    'node': "Device record",
                    'mesh': "Device group record",
                    'user': "User records",
                    'sysinfo': "Device information records",
                    'iploc': "IP location information records",
                    'note': "Text notes records",
                    'ifinfo': "Network interface information records",
                    'cfile': "Configuration file records",
                    'lastconnect': "Last connection time records",
                    'power': "Device power transition records",
                    'events': "Event records",
                    'serverstats': "Server statistics records",
                    'ugrp': "User group records",
                    'deviceshare': "Device sharing records",
                    'logintoken': "Account login token records",
                    'smbios': "Device SMBIOS record",
                    'pmt': "Device Push Notification Record",
                    'meshcentral': "Device, users, groups and other records"
                }
                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 "Pääte";
                if (v == 2) return "Työpöytä";
                if (v == 5) return "Tiedostot";
                if (v == 6) return "Järjestelmänvalvojan PowerShell";
                if (v == 8) return "Käyttäjän kuori";
                if (v == 9) return "Käyttäjä PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "sanansaattaja";
                if (v == 201) return "Verkko-RDP";
                if (v == 202) return "Web-SSH";
                if (v == 203) return "Web-SFTP";
                if (v == 204) return "Web-VNC";
                return "Tuntematon" + ' (' + 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 "Tuntematon käyttäjä";
                }
            }
            if (f == 'msg') {
                if (v == 107) return "Onnistunut kirjautuminen";
                if (v == 108) return "Väärä 2. tekijä";
                if (v == 109) return "Lukittu tili";
                if (v == 110) return "Virheellinen kirjautumisyritys";
            }
            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>' + "Tällä hetkellä ei ole ilmoituksia" + '</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("Tapahtui {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="' + "Kuittaa tämä ilmoitus" + '" 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">' + "Tyhjennä kaikki" + '</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,
                "Uusi tili", // 1
                "Palvelinraja",
                "Turvallisuusvaroitus",
                "Tilin asetukset",
                "Laiteryhmä",
                "Kutsu koodit"
            ];
            var translatedMessages = [
                null,
                "Lupa kielletty", // 1
                "Virheellinen käyttäjätunnus",
                "Väärä salasana",
                "Virheellinen sähköpostiosoite",
                "Virheellinen verkkotunnus",
                "Virheelliset sivuston käyttöoikeudet",
                "Käyttäjä on jo olemassa",
                "Käyttäjää ei voi lisätä tässä tilassa",
                "Vahvistuspoikkeus",
                "Tiliraja saavutettu.", // 10
                "Chat -pyyntö, hyväksy napsauttamalla tätä.",
                "Tällä tilillä on {0} epäonnistunutta kirjautumisyritystä viimeisen kirjautumisen jälkeen.",
                "Sähköpostiosoitteen vaihtaminen epäonnistui, toinen tili jo käyttää: {0}.",
                "Sähköposti lähetetty.",
                "Käyttäjää {0} ei löydy.",
                "Käyttäjiä {0} ei löydy.",
                "Virhe, ei voi vaihtaa aiemmin käytettyyn salasanaan.",
                "Virhe, ei voi vaihtaa yleisesti käytettyyn salasanaan.",
                "Virhe, salasanaa ei muutettu.",
                "Salasana vaihdettu.", // 20
                "Nykyinen salasana ei ole oikea.",
                "Virhe, kutsukoodi \"{0}\" on jo käytössä.",
                "SMS -yhdyskäytävä ei ole käytössä",
                "Ei käyttäjänhallintaoikeuksia",
                "Virheellinen tekstiviesti",
                "Tällä käyttäjällä ei ole puhelinnumeroa",
                "SMS succesfully sent.",
                "SMS -virhe",
                "SMS -virhe: {0}",
                "Sähköpostin verkkotunnus \"{0}\" ei ole sallittu. Vain ({1}) ovat sallittuja", // 30,
                "Invalid message",
                "Message succesfully sent.",
                "Message error",
                "Message error: {0}"
            ];
            if (typeof n.titleid == 'number') { try { n.title = translatedTitles[n.titleid]; } catch (ex) { } }
            if (typeof n.msgid == 'number') { try { n.text = translatedMessages[n.msgid]; if (Array.isArray(n.args)) { n.text = format(n.text, n.args[0], n.args[1], n.args[2], n.args[3], n.args[4], n.args[5]); } } catch (ex) { } }

            // Show notification within the web page.
            if (n.time == null) { n.time = Date.now(); }
            if (n.id == null) { n.id = Math.random(); }
            notifications.unshift(n);
            setNotificationCount(notifications.length);
            clickNotificationIcon(true);
            var notifySettings = getstore('notifications', 0);
            if (notifySettings & 1) { Q('chimes').play(); }

            // If web notifications are granted, use it.
            var notification = null;
            if (Notification && (Notification.permission == 'granted')) {
                var text = n.text.split('&reg;').join('').split('<b>').join('').split('</b>').join('').split('<br />').join('\r\n'); // Clean up any HTML codes
                if (n.nodeid) {
                    var node = getNodeFromId(n.nodeid);
                    if (node) {
                        if (notifySettings & 16) { // Notify with group name
                            notification = new Notification(decodeURIComponent('{{{extitle}}}') + ' - ' + meshes[node.meshid].name + ' - ' + node.name, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + node.icon + '.png' });
                        } else {
                            notification = new Notification(decodeURIComponent('{{{extitle}}}') + ' - ' + node.name, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + node.icon + '.png' });
                        }
                    }
                } else {
                    if (n.icon == null) { n.icon = 0; }
                    var title = n.title;
                    if (title == null) { title = ''; } else { title = ' - ' + n.title; }
                    notification = new Notification(decodeURIComponent('{{{extitle}}}') + title, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + n.icon + '.png' });
                }
                notification.id = n.id;
                notification.xtag = n.tag;
                notification.url = n.url;
                notification.nodeid = n.nodeid;
                notification.username = n.username;
                notification.onclick = function (e) { notificationSelected(e.target.id, true); }
                n.notification = notification;
            }

            // If the notification has a max time, setup the timer here.
            if ((typeof n.maxtime == 'number') && (n.maxtime > 0)) { var trigger = function notifyRemoveTrigger() { notificationDelete(notifyRemoveTrigger.xid); }; trigger.xid = n.id; setTimeout(trigger, n.maxtime * 1000); }
        }

        // Remove all notifications
        function deleteAllNotifications() {
            notifications = [];
            setNotificationCount(0);
            drawNotifications();
            QV('notifiyBox', false);
        }

        //
        // MyServer General
        //

        function setupGeneralServerStats() {
            // Destroy existing charts if they exist
            if (window.serverStatCpu) {
                window.serverStatCpu.destroy();
            }
            if (window.serverStatMemory) {
                window.serverStatMemory.destroy();
            }

            // Create new charts
            window.serverStatCpu = new Chart(document.getElementById('serverCpuChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#AAAAAA', '#00AA00'] }], labels: ["Käytetty", "Vapaata"] },
                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: ["Käytetty", "Vapaata"] },
                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: "Palvelimen Tila",
                AgentErrorCounters: "Agentin virhe laskurit",
                UnknownGroup: "Tuntematon ryhmä",
                InvalidPKCSsignature: "Virheellinen PKCS-allekirjoitus",
                InvalidRSAsignature: "Virheellinen RSA-allekirjoitus",
                InvalidJSON: "Virheellinen JSON",
                UnknownAction: "Tuntematon toimenpide",
                BadWebCertificate: "Huono Web-varmenne",
                BadSignature: "Huono allekirjoitus",
                MaxSessionsReached: "Istuntojen enimmäismäärä saavutettu",
                UnknownDeviceGroup: "Tuntematon laiteryhmä",
                InvalidDeviceGroupType: "Virheellinen laiteryhmän tyyppi",
                DuplicateAgent: "Monista Agentti",
                ConnectedIntelAMT: "Yhdistetyt Intel&reg; AMT",
                ConnectedIntelAMTCira: "Yhdistetyt Intel&reg; AMT CIRA",
                RelayErrors: "Releen virheet",
                UserAccounts: "Käyttäjätilit",
                DeviceGroups: "Laiteryhmät",
                AgentSessions: "Agenttien istunnot",
                ConnectedUsers: "Yhdistetyt käyttäjät",
                UsersSessions: "Käyttäjien istuntoja",
                RelaySessions: "Viestiistunnot",
                RelayCount: "Viesti"
            };

            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>' + "Prosessori kuorma" + '</div><div><b title="' + "Prosessorin kuorma viimeisen minuutin aikana" + '">' + (Math.round(message.cpuavg[0] * 100.0) / 100.0) + '</b>, <b title="' + "Prosessorin kuorma viimeisen 5 minuutin aikana" + '">' + (Math.round(message.cpuavg[1] * 100.0) / 100.0) + '</b>, <b title="' + "Prosessorin kuorma viimeisen 15 minuutin aikana" + '">' + (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>' + "Käytettävissä oleva muisti" + '</div><div><b>' + getNiceSize3(message.availablemem) + '</b> ' + "vapaata" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "yhteensä" + '</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>' + "Muisti" + '</div><div><b>' + getNiceSize3(message.freemem) + '</b> ' + "vapaata" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "yhteensä" + '</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 = ["Agentti", "CIRA", "AMT-OS", "HTTP", "Rele", "Pääte", "Työpöytä", "Tiedostot", "WebRDP", "WebSSH", "WebVNC", "Multiplex -pöytäkone"];
        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 = "Yhteysmäärä";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Agentit", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Käyttäjät", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Käyttäjäistunnot", data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: "Viestiistunnot", 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 = "Megatavua";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Ulkoinen", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Pinoa käytetty", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Pino yhteensä", 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 = "Megatavua";
                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 = "Käyttö";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Prosessori", 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' : '') + '>' + "Tyhjä" + '</option>'; }
                QH('p40server', x);
            }
            QV('p40server', servers.length > 0);
        }

        function p40downloadEvents() {
            var csv = "aika, yhteyshenkilö, yhteyskäyttäjät, yhteyskäyttäjät, yhteysviestien välitys, yhteysosoitteet, ulkoiset muistit, muistiaheapused, muistiasetukset, muistit" + '\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', "Palvelimen jäljitystapahtuma", '<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>' + "Ydin palvelin" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c1 ' + ((serverTraceSources.indexOf('cookie') >= 0) ? 'checked' : '') + '>' + "Evästekooderi" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c2 ' + ((serverTraceSources.indexOf('dispatch') >= 0) ? 'checked' : '') + '>' + "Viestin lähettäjä" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c3 ' + ((serverTraceSources.indexOf('main') >= 0) ? 'checked' : '') + '>' + "Pääpalvelimen viestit" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c4 ' + ((serverTraceSources.indexOf('peer') >= 0) ? 'checked' : '') + '>' + "MeshCentral Server -vertailu" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c15 ' + ((serverTraceSources.indexOf('agent') >= 0) ? 'checked' : '') + '>' + "MeshAgent liikenne" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c14 ' + ((serverTraceSources.indexOf('agentupdate') >= 0) ? 'checked' : '') + '>' + "MeshAgent päivitys" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c16 ' + ((serverTraceSources.indexOf('cert') >= 0) ? 'checked' : '') + '>' + "Palvelimen sertifikaatti" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c17 ' + ((serverTraceSources.indexOf('db') >= 0) ? 'checked' : '') + '>' + "Palvelimen tietokanta" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c18 ' + ((serverTraceSources.indexOf('email') >= 0) ? 'checked' : '') + '>' + "Sähköposti/tekstiviesti/push -liikenne" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Web-palvelin" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c5 ' + ((serverTraceSources.indexOf('web') >= 0) ? 'checked' : '') + '>' + "Web-palvelin" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c6 ' + ((serverTraceSources.indexOf('webrequest') >= 0) ? 'checked' : '') + '>' + "Web-palvelimen pyynnöt" + '</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' : '') + '>' + "Verkkopalvelimen HTTP -otsikot" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c21 ' + ((serverTraceSources.indexOf('authlog') >= 0) ? 'checked' : '') + '>' + "User Authentication Log" + '</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 -päällikkö" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c9 ' + ((serverTraceSources.indexOf('webrelay') >= 0) ? 'checked' : '') + '>' + "Yhteysrele" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c10 ' + ((serverTraceSources.indexOf('mps') >= 0) ? 'checked' : '') + '>' + "CIRA-Palvelin" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c11 ' + ((serverTraceSources.indexOf('mpscmd') >= 0) ? 'checked' : '') + '>' + "CIRA-palvelimen komennot" + '</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', "Palvelimen jäljitys", 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 = "aika, lähde, viesti" + '\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, "Tilin Turvallisuus", 1, null, "Unable to access this feature until two-factor authentication is enabled. This is required for extra security. Go to the \"My Account\" and look at the \"Account Security\" section."); return; }
                go(5);
            }
            if (idtype == 'server') { go(6); }
        }

        function go(x, event) {
            if (event && ((event.which == 3) || (event.button == 2))) return; // ignore right clicks as they are handled elsewhere
            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0) && (x > 2)) {
                x = 2;
                setModalContent('xxAddAgent', "Tilin Turvallisuus", '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="' + "Versio yhteensopimaton, päivitä ensin MeshCentral-asennus" + '" 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="' + "Versio yhteensopimaton, päivitä ensin MeshCentral-asennus" + '" 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 = "Tarkistetaan...";
                    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="' + "Näytä muutosloki" + '" rel="noreferrer noopener" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
                        } else {
                            cant_action.push('upgrade');
                            if (p.status) p.upgradeAvail = "Ajantasalla";
                            else p.upgradeAvail = '<a title="' + "Näytä muutosloki" + '" 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', "Laajennuksen toiminto", format("Haluatko varmasti {0} laajennuksen: {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;">' + "Lisäosan käyttöoikeudet" + ' - ' + 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> ' + "Ladataan käyttöoikeuksia" + '...</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;">' + "Oikeuksien kaskadi:" + ' Node → Mesh → Global → Default' + '</div>';
            x += '<div>';
            x += '<button type="button" class="btn btn-secondary" style="margin-right:10px;" onclick="closePluginPermModal()">' + "Peruuta" + '</button>';
            x += '<button type="button" class="btn btn-primary" onclick="savePluginPermissionsEx()">' + "Tallenna" + '</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">' + "Tämä lisäosa ei ole määrittänyt mitään käyttöoikeuksia." + '</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\')">' + "Globaali" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary me-1" id="btnTabMeshes" onclick="switchPermTab(\'meshes\')">' + "Ryhmät" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary" id="btnTabNodes" onclick="switchPermTab(\'nodes\')">' + "Solmut" + '</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' : '') + '>✓ ' + "Sallittu" + '</option>';
                x += '<option value="denied"' + (defaultVal === 'denied' ? ' selected' : '') + '>✗ ' + "Evätty" + '</option>';
                x += '<option value="inherited"' + (defaultVal === 'inherited' ? ' selected' : '') + '>↩ ' + "Peritty" + '</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">' + "Sallittu" + '</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">' + "Evätty" + '</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="' + "Etsi käyttäjiä, ryhmiä, laitteita" + '..." ';
            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' ? "Valitse laiteohjelma" : "Valitse laite" ) + ' --</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' ? "Tämän laitetyhmän erityisoikeudet" : "Tämän laitteen erityisoikeudet") + '</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("Soitto virhe", 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>