<!DOCTYPE html><html lang="de" 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>Information</b></div>
        <div id="cxdesktop" class="cmtext" onclick="cmaction(3,event)">Desktop</div>
        <div id="cxterminal" class="cmtext" onclick="cmaction(2,event)">Terminal</div>
        <div id="cxfiles" class="cmtext" onclick="cmaction(4,event)">Dateien</div>
        <div id="cxevents" class="cmtext" onclick="cmaction(5,event)">Ereignisse</div>
        <div id="cxdetails" class="cmtext" onclick="cmaction(6,event)">Details</div>
        <div id="cxconsole" class="cmtext" onclick="cmaction(7,event)">Konsole</div>
        <div id="cxwebrdp" class="cmtext" onclick="cmaction(11,event)">Web-RDP</div>
        <div id="cxwebvnc" class="cmtext" onclick="cmaction(12,event)">Web-VNC</div>
        <div id="cxwebssh" class="cmtext" onclick="cmaction(13,event)">Web-SSH</div>
        <div id="cxrdp" class="cmtext" onclick="cmaction(14,event)">RDP</div>
        <div id="cxplugins" class="cmtext" onclick="cmaction(8,event)" style="display:none">Plugins</div>
        <hr id="cxmgroupsplit">
        <div id="cxstar" class="cmtext" onclick="cmaction(10,event)" style="display:none">Markierung umschalten</div>
        <div id="cxmdesktop" class="cmtext" onclick="cmaction(9,event)" style="display:none">Multi-Desktop</div>
    </div>
    <div id="meshContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div id="cxselectall" class="cmtext" onclick="cmmeshaction(1,event)">Alle auswählen</div>
        <div id="cxselectnone" class="cmtext" onclick="cmmeshaction(2,event)">Auswahl aufheben</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)">Alternativer Port</div>
    </div>
    <div id="sshPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmsshportaction(1,event)">Alternativer Port</div>
    </div>
    <div id="rfbPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmrfbportaction(1,event)">Alternativer Port</div>
    </div>
    <div id="httpPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpportaction(1,event)">Alternativer Port</div>
    </div>
    <div id="httpsPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmhttpsportaction(1,event)">Alternativer Port</div>
    </div>
    <div id="filesContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmfilesaction(1,event)">Umbenennen</div>
        <div class="cmtext" onclick="cmfilesaction(2,event)">Bearbeiten</div>
        <div class="cmtext" onclick="cmfilesaction(3,event)">Löschen</div>
    </div>
    <div id="expandAllContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmexpandaction(1,event)">Alle erweitern</div>
        <div class="cmtext" onclick="cmexpandaction(2,event)">Alle reduzieren</div>
    </div>
    <div id="deskPlayerContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskplayeraction(1,event)">Player öffnen...</div>
    </div>
    <div id="deskKeyShortcutContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
        <div class="cmtext" onclick="cmdeskshortcutaction(1,event)">Anpassen...</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)">Anpassen...</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="Hier klicken, um aktuelle Benachrichtigungen anzuzeigen" 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>Meine Geräte</span>
                                </div>

                                <div class="userDropdownMenuItem userDropdownMobileOnly mobile-menu-item" data-action="events" onclick="handleUserMenuItem('events')">
                                    <i class="fa fa-calendar userDropdownMenuIcon"></i>
                                    <span>Meine Ereignisse</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>Meine Benutzer</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>Meine Dateien</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>Mein Server</span>
                                </div>

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

                                <div class="userDropdownMenuItem userDropdownUISettings" onclick="toggleUISubmenu(); event.stopPropagation();">
                                    <i class="fa fa-sliders userDropdownMenuIcon"></i>
                                    <span>UI-Einstellungen</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>Schalten Sie die moderne Benutzeroberfläche um</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>Schnittstelle der linken Leiste</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="top-bar" onclick="handleUserMenuItem('top-bar')">
                                        <i class="fa fa-window-maximize userDropdownMenuIcon"></i>
                                        <span>Oberfläche mit Leiste oben</span>
                                    </div>
                                    <div class="userDropdownMenuItem" data-action="fixed-width" onclick="handleUserMenuItem('fixed-width')">
                                        <i class="fa fa-columns userDropdownMenuIcon"></i>
                                        <span>Oberfläche mit fester Breite</span>
                                    </div>
                                    <div id="toggleFooterMenuItem" class="userDropdownMenuItem" data-action="toggle-footer" onclick="handleUserMenuItem('toggle-footer')">
                                        <i class="fa fa-window-minimize userDropdownMenuIcon"></i>
                                        <span>Fußzeile ein-/ausschalten</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">Nachtmodus ein-/ausschalten</span>
                                </div>


                                <div class="userDropdownMenuItem" data-action="notes" onclick="handleUserMenuItem('notes')">
                                    <i class="fa fa-sticky-note userDropdownMenuIcon"></i>
                                    <span>Persönliche Notizen</span>
                                </div>

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

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

                                <div class="userDropdownMenuItem" data-action="logout" onclick="handleUserMenuItem('logout')">
                                    <i class="fa fa-sign-out userDropdownMenuIcon"></i>
                                    <span>Abmelden</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)">Meine Geräte</td>
                            <td tabindex="0" id="MainMenuMyAccount" class="topbar_td style3x" onmouseup="go(2,event)" onkeypress="if (event.key == 'Enter') go(2)">Mein Konto</td>
                            <td tabindex="0" id="MainMenuMyEvents" class="topbar_td style3x" onmouseup="go(3,event)" onkeypress="if (event.key == 'Enter') go(3)">Meine Ereignisse</td>
                            <td tabindex="0" id="MainMenuMyFiles" class="topbar_td style3x" onmouseup="go(5,event)" onkeypress="if (event.key == 'Enter') go(5)">Meine Dateien</td>
                            <td tabindex="0" id="MainMenuMyUsers" class="topbar_td style3x" onmouseup="go(4,event)" onkeypress="if (event.key == 'Enter') go(4)">Meine Benutzer</td>
                            <td tabindex="0" id="MainMenuMyServer" class="topbar_td style3x" onmouseup="go(6,event)" onkeypress="if (event.key == 'Enter') go(6)">Mein Server</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)">Allgemein</td>
                                        <td tabindex="0" id="MainDevDesktop" class="topbar_td style3x" onmouseup="go(11,event)" onkeypress="if (event.key == 'Enter') go(11)">Desktop</td>
                                        <td tabindex="0" id="MainDevTerminal" class="topbar_td style3x" onmouseup="go(12,event)" onkeypress="if (event.key == 'Enter') go(12)">Terminal</td>
                                        <td tabindex="0" id="MainDevFiles" class="topbar_td style3x" onmouseup="go(13,event)" onkeypress="if (event.key == 'Enter') go(13)">Dateien</td>
                                        <td tabindex="0" id="MainDevEvents" class="topbar_td style3x" onmouseup="go(16,event)" onkeypress="if (event.key == 'Enter') go(16)">Ereignisse</td>
                                        <td tabindex="0" id="MainDevInfo" class="topbar_td style3x" onmouseup="go(17,event)" onkeypress="if (event.key == 'Enter') go(17)">Details</td>
                                        <td tabindex="0" id="MainDevApps" class="topbar_td style3x" onmouseup="go(18,event)" onkeypress="if (event.key == 'Enter') go(18)">Software</td>
                                        <td tabindex="0" id="MainDevAmt" class="topbar_td style3x" onmouseup="go(14,event)" onkeypress="if (event.key == 'Enter') go(14)">Intel®AMT</td>
                                        <td tabindex="0" id="MainDevConsole" class="topbar_td style3x" onmouseup="go(15,event)" onkeypress="if (event.key == 'Enter') go(15)">Konsole</td>
                                        <td tabindex="0" id="MainDevPlugins" class="topbar_td style3x" onmouseup="go(19,event)" onkeypress="if (event.key == 'Enter') go(19)">Plugins</td>
                                        <td class="topbar_td_end style3">&nbsp;</td>
                                    </tr>
                                </tbody></table>
                            </div>
                            <button class="mobile-submenu-arrow" onclick="mobileSubMenuScroll('MainSubMenu',1)" aria-label="Scroll right"><i class="fa fa-caret-square-right"></i></button>
                        </div>
                    </div>
                    <div id="MeshSubMenuSpan" style="display:none">
                        <table id="MeshSubMenu" cellpadding="0" cellspacing="0" class="style1">
                            <tbody><tr>
                                <td tabindex="0" id="MeshGeneral" class="topbar_td style3x" onmouseup="go(20,event)" onkeypress="if (event.key == 'Enter') go(20)">Allgemein</td>
                                <td tabindex="0" id="MeshSummary" class="topbar_td style3x" onmouseup="go(21,event)" onkeypress="if (event.key == 'Enter') go(21)">Zusammenfassung</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)">Ereignisse</td>
                                <td tabindex="0" id="EventsReport" class="topbar_td style3x" onmouseup="go(60,event)" onkeypress="if (event.key == 'Enter') go(60)">Berichte</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)">Allgemein</td>
                                <td tabindex="0" id="UserEvents" class="topbar_td style3x" onmouseup="go(31,event)" onkeypress="if (event.key == 'Enter') go(31)">Ereignisse</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)">Benutzer</td>
                                <td tabindex="0" id="UsersGroups" class="topbar_td style3x" onmouseup="go(50,event)" onkeypress="if (event.key == 'Enter') go(50)">Gruppen</td>
                                <td tabindex="0" id="UsersRecordings" class="topbar_td style3x" onmouseup="go(52,event)" onkeypress="if (event.key == 'Enter') go(52)">Aufnahmen</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)">Allgemein</td>
                                <td tabindex="0" id="ServerStats" class="topbar_td style3x" onmouseup="go(40,event)" onkeypress="if (event.key == 'Enter') go(40)">Statistiken</td>
                                <td tabindex="0" id="ServerConsole" class="topbar_td style3x" onmouseup="go(115,event)" onkeypress="if (event.key == 'Enter') go(115)">Konsole</td>
                                <td tabindex="0" id="ServerTrace" class="topbar_td style3x" onmouseup="go(41,event)" onkeypress="if (event.key == 'Enter') go(41)">Trace</td>
                                <td tabindex="0" id="ServerPlugins" class="topbar_td style3x" onmouseup="go(42,event)" onkeypress="if (event.key == 'Enter') go(42)">Plugins</td>
                                <td class="topbar_td_end style3">&nbsp;</td>
                            </tr>
                        </tbody></table>
                    </div>
                </div>
            </div>
        </div>
        <div id="column_l" class="pt-3">
            <div id="p0" style="display:none">
                <div id="p0message"><span id="p0span">Server getrennt</span>, <href onclick="reload()" style="cursor:pointer"><u>zum Wiederverbinden klicken</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">Meine Geräte</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="Gehe zur ersten Seite" 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="Gehe zur vorherigen Seite" 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="Gehe zur nächsten Seite" 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="Gehe zur letzten Seite" 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="Spalten">
                            <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="Liste">
                            <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="Desktops">
                            <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="Desktops">
                            <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="Karte" 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="Alle reduzieren" 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="Alle erweitern" 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="Alle auswählen">
                        <input type="button" class="btn btn-primary me-1 btn-sm" id="GroupActionButton" disabled="disabled" value="Gruppenaktion" 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="Filter" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Geräte nach Name oder Präfix filtern: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Präfix ! zum Ausschließen, z.B. !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="Gerätefilter">
                            <option value="0" selected="">Alle</option>
                            <option value="1">Online</option>
                            <option value="5">Offline</option>
                            <option value="2">Sitzungen</option>
                            <option value="3">Markiert</option>
                            <option value="4">Intel® AMT</option>
                            <option value="6">Hilfe</option>
                            <option value="7">Tagged</option>
                            <option value="8">Ohne Tags</option>
                        </select>
                        <div class="form-check" title="Geräte-Betriebssystemname anzeigen"><input class="form-check-input me-1" type="checkbox" id="RealNameCheckBox" onclick="onRealNameCheckBox()"><label class="form-check-label" for="RealNameCheckBox">Name im Betriebssystem</label></div>
                        <label style="display:none"><input type="checkbox" id="OnlineCheckBox" class="form-check-input me-2" onclick="onOnlineCheckBox(event)"><span title="Zeigen Sie nur Geräte an, die online sind">Online</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="Alle reduzieren" 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="Alle erweitern" 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="Alle verbinden">
                            <input type="button" class="btn btn-danger me-1 btn-sm" onclick="disconnectAllKvmFunction()" value="Alle trennen">
                            <div class="form-check form-check-inline me-1 align-middle" title="Automatische Verbindung">
                                <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="Einstellungen">
                        <input type="text" id="KvmSearchInput" class="form-control-sm me-1" placeholder="Filter" onchange="onDeviceSearchChanged(event)" onclick="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" onfocus="onSearchFocus(1)" onblur="onSearchFocus(0)" title="Geräte nach Name oder Präfix filtern: user: (u:) ip: group: (g:) tag: (t:) atag: (a:) os: amt: desc: wsc:ok/noav/noupdate/nofirewall/any connectivity: (c:) - Präfix ! zum Ausschließen, z.B. !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="Standortsuche" onfocus="onMapSearchFocus(1)" onblur="onMapSearchFocus(0)">
                        <input class="btn btn-primary me-1 btn-sm" type="button" value="Suche" title="Suche nach Standort" onclick="getSearchLocation()">
                        <input class="btn btn-secondary me-1 btn-sm" type="button" id="refreshmap" title="Kartenansicht zurücksetzen" value="Neustarten" onclick="refreshMap(false,true)">
                    </div>
                    <div class="d-flex align-items-center ms-auto">
                        <div style="display:none" id="devListToolbarView">
                            Ansicht
                            <select id="viewselect" onchange="onDeviceViewChange()">
                                <option value="1">Spalten</option>
                                <option value="2">Liste</option>
                                <option value="3">Feste Breite der Desktops</option>
                                <option id="viewselectmapoption" value="4" style="display:none">Karte</option>
                                <option value="5">Desktops</option>
                            </select>
                        </div>
                        <div style="display:none" id="devListToolbarSize">
                            Größe
                            <select id="sizeselect" class="form-select-sm me-2" onchange="onDeviceViewChange()">
                                <option value="0">Klein</option>
                                <option value="1">Mittel</option>
                                <option value="2">Groß</option>
                            </select>
                            &nbsp;
                        </div>
                        <div style="display:none" id="devListToolbarSort">
                            Sortieren
                            <select id="sortselect" class="form-select-sm me-2" onchange="mainUpdate(6)">
                                <option>Gruppe</option>
                                <option>Energiezustand</option>
                                <option>Gerät</option>
                                <option>Tags</option>
                                <option>Gruppen-Tags</option>
                                <option>Zuletzt gesehen</option>
                                <option>Letzter Startvorgang</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">Um loszulegen, <a href="#" onclick="return account_createMesh()"><strong>hier klicken, um eine Gerätegruppe zu erstellen</strong></a>.</div>
                                <div id="getStarted2">Keine Gerätegruppen.</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">Standortergebnis</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>Mein Konto</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)">Ändere das Bild</a></div>
                    </div>
                    <div id="p2AccountSecurity" style="display:none">
                        <p><strong>Konto-Sicherheit</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()">Telefonnummer verwalten</a><br></span>
                            </div>
                            <div id="manageEmail2FA">
                                <div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthEmail()">E-Mail-Authentifizierung verwalten</a><br></span>
                            </div>
                            <div id="manageAuthApp">
                                <div class="p2AccountActions"><span id="authAppSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthApp()">Authentifikator-App verwalten</a><br></span>
                            </div>
                             <div id="manageDuoApp">
                                <div class="p2AccountActions"><span id="authDuoSetupCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageAuthDuo()">Duo-Authentifizierung verwalten</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)">Sicherheitsschlüssel verwalten</a><br></span>
                            </div>
                            <div id="managePushAuthDev">
                                <div class="p2AccountActions"><span id="authPushAuthDevCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_managePushAuthDev()">Push-Authentifizierung verwalten</a><br></span>
                            </div>
                            <div id="manageMessaging1">
                                <div class="p2AccountActions"><span id="authMessagingCheck"><strong>✓</strong></span></div><span><a href="#" onclick="return account_manageMessaging()">Benachrichtigungen verwalten</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)">Sicherungscodes verwalten</a><br></span>
                            </div>
                            <div class="p2AccountActions"></div><span><a href="#" onclick="return account_viewPreviousLogins()">Vorherige Anmeldungen anzeigen</a><br></span>
                        </div>
                    </div>
                    <div id="p2AccountActions">
                        <p><strong>Konto-Aktionen</strong></p>
                        <p class="mL">
                            <span id="managePhoneNumber2" style="display:none"><a href="#" onclick="return account_managePhone()">Telefonnummer verwalten</a><br></span>
                            <span id="manageMessaging2" style="display:none"><a href="#" onclick="return account_manageMessaging()">Benachrichtigungen verwalten</a><br></span>
                            <span id="verifyEmailId" style="display:none"><a href="#" onclick="return account_showVerifyEmail()">E-Mail verifizieren</a><br></span>
                            <span id="accountEnableNotificationsSpan" style="display:none"><a href="#" onclick="return account_enableNotifications()">Web-Benachrichtigungen aktivieren</a><br></span>
                            <a href="#" onclick="return account_showLocalizationSettings()">Lokalisierungseinstellungen</a><br>
                            <a href="#" onclick="return account_showAccountNotifySettings()">Benachrichtungseinstellungen</a><br>
                            <span id="p2AccountPassActions">
                                <span id="accountChangeEmailAddressSpan" style="display:none"><a href="#" onclick="return account_showChangeEmail()">E-Mail-Adresse ändern</a><br></span>
                                <a href="#" onclick="return account_showChangePassword()">Passwort ändern</a><span id="p2nextPasswordUpdateTime"></span><br>
                                <a href="#" onclick="return account_showDeleteAccount()">Konto löschen</a><br>
                            </span>
                            <span id="accountCreateLoginTokenSpan" style="display:none"><a href="#" onclick="return account_createLoginToken()">Login-Token erstellen</a><br></span>
                            <a href="#" onclick="return account_showThemesSwitcher()">Theme ändern</a><br>
                            <span id="accountCustomIconsSpan" style="display:none"><a href="#" onclick="return showIconCustomization()">Symbole anpassen</a><br></span>
                        </p>
                    </div>
                    <div id="p2logintokens"></div>
                    <strong>Gerätegruppen</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> Neu</button></span>
                    <br><br>
                    <div id="p2meshes"></div>
                    <div id="p2noMeshFound" style="display:none">Keine Gerätegruppen.<span id="p2createMeshLink2"> <a href="#" onclick="return account_createMesh()"><strong>Fang hier an!</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">Meine Ereignisse</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">Filter</label>
                                <select id="p3filterevents" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="">Alle Protokolle</option>
                                    <option notransval="1" value="agentlog">Agenten-Protokoll</option>
                                    <option notransval="1" value="relaylog">Relay-Protokoll</option>
                                    <option notransval="1" value="manual">Manuelles Protokoll</option>
                                    <option notransval="1" value="runcommands">Befehls-Protokoll</option>
                                    <option notransval="1" value="batchupload">Batch Upload Protokoll</option>
                                    <option notransval="1" value="changenode">Knotenänderungs-Protokoll</option>
                                    <option notransval="1" value="removenode">Knoten-Entfernungs-Protokoll</option>
                                </select>
                                <label for="p3limitdropdown" class="me-1">Anzeigen</label>
                                <select id="p3limitdropdown" class="form-select-sm me-2" onchange="refreshEvents()">
                                    <option notransval="1" value="60">Letzte 60</option>
                                    <option notransval="1" value="120">Letzte 120</option>
                                    <option notransval="1" value="250">Letzte 250</option>
                                    <option notransval="1" value="500">Letzte 500</option>
                                    <option notransval="1" value="1000">Letzte 1000</option>
                                    <option notransval="1" value="">Keine Begrenzung</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(2)"><i class="fa-solid fa-download" title="Ereignisse herunterladen"></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">Meine Benutzer</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="Alle auswählen">
                                <input type="button" id="UsersGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Gruppenaktion" onclick="p3usersGroupActionFunction()">
                                <input id="UserNewAccountButton" class="btn btn-success btn-sm me-1" type="button" onclick="showCreateNewAccountDialog()" value="Neues Konto...">
                                <div class="btn-group me-1 mobileActionDropdown">
                                    <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">Mehr&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
                                    <div class="dropdown-menu">
                                        <button type="button" class="dropdown-item" onclick="showUserBroadcastDialog()">Rundruf</button>
                                        <button type="button" class="dropdown-item" onclick="p4downloadUserInfo()">Benutzerinformationen herunterladen</button>
                                        <button type="button" id="p4MobileUserBatchCreate" class="dropdown-item" style="display:none" onclick="p4batchAccountCreate()">Konten stapelweise erstellen</button>
                                        <button type="button" class="dropdown-item" onclick="onUsersViewSettings()">Einstellungen</button>
                                    </div>
                                </div>
                                <div class="d-inline-flex align-items-center">
                                    <input id="UserSearchInput" type="search" class="form-control-sm me-1" placeholder="Filter" 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="Rundruf">
                                <a href="#" onclick="p4downloadUserInfo()"><i role="button" class="fa-solid fa-download me-2" title="Benutzerinformationen herunterladen"></i></a>
                                <a href="#" onclick="p4batchAccountCreate()"><i role="button" id="p4UserBatchCreate" style="display:none" title="Stapel-Erzeugung vieler Benutzerkonten" 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="Einstellungen" 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">Meine Dateien</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="Hoch">
                                <input type="button" id="p5SelectAllButton" class="btn btn-secondary btn-sm me-1" disabled="disabled" onclick="p5selectallfile();" value="Alle auswählen">
                                <input type="button" id="p5RenameFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Umbenennen" onclick="p5renamefile();">
                                <input type="button" id="p5DeleteFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Löschen" onclick="p5deletefile();">
                                <input type="button" id="p5ViewFileButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Bearbeiten" onclick="p5viewfile()">
                                <input type="button" id="p5NewFolderButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Neuer Ordner" onclick="p5createfolder();">
                                <input type="button" id="p5UploadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Hochladen" onclick="p5uploadFile()">
                                <input type="button" id="p5DownloadButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Herunterladen" onclick="p5downloadButton()">
                                <input type="button" id="p5CutButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Ausschneiden" onclick="p5copyFile(1)">
                                <input type="button" id="p5CopyButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Kopie" onclick="p5copyFile(0)">
                                <input type="button" id="p5PasteButton" class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Einfügen" 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">Mehr&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()">Neuer Ordner</button>
                                        <button type="button" disabled="disabled" id="p5MobileUploadButton" class="dropdown-item" onclick="p5uploadFile()">Hochladen</button>
                                        <button type="button" disabled="disabled" id="p5MobileDownloadButton" class="dropdown-item" onclick="p5downloadButton()">Herunterladen</button>
                                        <button type="button" disabled="disabled" id="p5MobileCutButton" class="dropdown-item" onclick="p5copyFile(1)">Ausschneiden</button>
                                        <button type="button" disabled="disabled" id="p5MobileCopyButton" class="dropdown-item" onclick="p5copyFile(0)">Kopie</button>
                                        <button type="button" disabled="disabled" id="p5MobilePasteButton" class="dropdown-item" onclick="p5pasteFile()">Einfügen</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">Nach Name sortieren</option>
                                    <option value="2">Nach Größe sortieren</option>
                                    <option value="3">Nach Datum sortieren</option>
                                    <option value="4">Nach Name absteigend</option>
                                    <option value="5">Nach Größe absteigend</option>
                                    <option value="6">Nach Datum absteigend</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>Diese Dateien werden öffentlich geteilt, klicken Sie auf "Link", um die öffentliche URL zu erhalten.</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">Mein Server</div>
                        <img id="MainMeshImage" src="serverpic.ashx" class="ms-auto">
                    </div>
                    <div id="p2ServerActions">
                        <p><strong>Server-Aktionen</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()">Server-Datensicherung herunterladen</a>
                            </div>
                            <div id="p2ServerActionsRestore">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showRestoreDlg()">Server aus Datensicherung wiederherstellen</a>
                            </div>
                            <div id="p2ServerActionsGoogleBackup" style="display:none">
                                <div class="p2AccountActions"><span id="p2ServerActionsGoogleBackupCheck" style="display:none"><strong>✓</strong></span></div><span><a href="#" onclick="return server_setupGoogleDriveBackup()">Google Drive-Sicherung</a><br></span>
                            </div>
                            <div id="p2ServerActionsVersion">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showVersionDlg()">Server auf Aktualisierung überprüfen</a>
                            </div>
                            <div id="p2ServerActionsErrors">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showErrorsDlg()">Server-Fehlerprotokoll anzeigen</a>
                            </div>
                            <div id="p2ServerActionsConfig">
                                <div class="p2AccountActions"><span style="display:none"><strong>✓</strong></span></div><a href="#" onclick="return server_showConfigDlg()">Server-Konfiguration anzeigen</a>
                            </div>
                        </div>
                        <br>
                    </div>
                    <strong>Server-Statistiken</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>Server-Warnmeldungen</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="Zurück" 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>Allgemein<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>Notizen</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="Markiert" 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="Liste der auf diesem Computer aktiven Remotesitzungen." 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="Hilfe angefordert" 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="Zurück" 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>Desktop<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">ANZAHL</div>
                        <div id="p11capslock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">KAPPEN</div>
                        <div id="p11scrolllock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">SCROLLEN</div>
                    </div>
                    <div id="devListToolbarViewIcons">
                        <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick="deskToggleFull(event)" title="Vollbild. Halten Sie die Umschalttaste gedrückt, um das Browserfenster im Vollbild anzuzeigen."></i>
                    </div>
                </div>
                <div id="p11warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Weiterleitungsport für KVM ist deaktiviert<span id="p11warninga">, zum Aktivieren hier klicken.</span></div>
                </div>
                <div id="p11warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Entfernter Rechner ist nicht eingeschaltet, klicken Sie hier um einen Stromversorgungsbefehl abzusetzen.
                    </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="Auto-Verbindung" 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="Verbinden Sie sich mit MeshAgent Remote Desktop" onclick="connectDesktop(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">Verbinden</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">Dropdown umschalten</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(1,event)">Einwilligung anfordern + DS-Leiste</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(2,event)">Einwilligung anfordern</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(3,event)">Datenschutzleiste</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="Verbinden Sie sich über RDP" onclick="askRdpCredentials()" onkeypress="return false" onkeydown="return false" disabled="disabled">RDP verbinden</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">Dropdown umschalten</span></button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmaltportaction(1,event)">Alternativer Port</a></li>
                                </ul>
                            </div>
                            <span id="connectbutton1hspan"><button class="btn btn-primary btn-sm me-1" type="button" id="connectbutton1h" title="Verbindung über Hardware-KVM herstellen" onclick="connectDesktop(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">HW-Verbindung</button></span>
                            <div class="btn-group dropdown-center me-1" id="disconnectbutton1span">
                                <button type="button" class="btn btn-danger btn-sm" id="disconnectbutton1" title="Verbinden Sie sich mit MeshAgent Remote Desktop" onclick="connectDesktop(event,0)" onkeypress="return false" onkeydown="return false">Trennen</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">Dropdown umschalten</span>
                                </button>
                                <ul class="dropdown-menu">
                                    <li><a class="dropdown-item" onclick="cmdeskaction(10,event)">Trennen und sperren</a></li>
                                    <li><a class="dropdown-item" onclick="cmdeskaction(11,event)">Trennen</a></li>
                                </ul>
                            </div>
                            <span id="deskstatus" style="line-height:22px">Getrennt</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="Den Energiezustand der entfernten Maschine ändern" onkeypress="return false" onkeydown="return false" value="Energiezustandsaktionen" onclick="showPowerActionDlg()" style="display:none">
                            <input id="deskActionsBtn" type="button" class="btn btn-primary btn-sm me-1" title="Energiezustandsaktionen auf dem Gerät ausführen" onkeypress="return false" onkeydown="return false" value="Aktionen" onclick="deviceActionFunction()">
                            <input id="deskActionsSettings" type="button" class="btn btn-primary btn-sm me-1" value="Einstellungen" title="Einstellungen für entfernten Desktop bearbeiten" onkeypress="return false" onkeydown="return false" onclick="showDesktopSettings()">
                            <input id="deskFocusBtn" type="button" title="Fokusmodus ein-/ausschalten; wenn aktiv, wird nur die Region um die Maus aktualisiert" onkeypress="return false" onkeydown="return false" value="Alles fokussieren" onclick="deskToggleFocus()" style="margin-right:3px;display:none">
                            <div id="deskRecordIcon" class="deskareaicon" title="Server zeichnet diese Sitzung auf" style="display:none;background-color:red;width:12px;height:12px;border-radius:6px;margin-top:5px"></div>
                            <div class="deskareaicon" title="Nach rechts drehen" onclick="drotate(1)"><i class="fa-solid fa-rotate-right"></i></div>
                            <div class="deskareaicon" title="Nach links drehen" onclick="drotate(-1)"><i class="fa-solid fa-rotate-left"></i></div>
                            <div class="deskareaicon" title="Ansichtsmodus umschalten" 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()">Aktualisieren</a>
                                <div id="deskToolsTopTabProcess" class="deskToolsTopTab" onclick="changeDeskToolTab(0)" style="left:0px;bottom:0px">Prozesse</div>
                                <div id="deskToolsTopTabService" class="deskToolsTopTab" onclick="changeDeskToolTab(1)" style="display:none;left:90px;color:gray">Dienste</div>
                            </div>
                            <div id="deskToolsArea">
                                <div id="DeskToolsProcessTab">
                                    <div id="deskToolsHeader">
                                        <a class="colmn1" title="Nach Prozess-ID sortieren" onclick="sortProcess(0)">PID</a>
                                        <a class="colmn2" title="Nach Name sortieren" onclick="sortProcess(1)">Name</a>
                                    </div>
                                    <div id="DeskToolsProcesses" style=""></div>
                                </div>
                                <div id="DeskToolsServiceTab" style="display:none">
                                    <div id="deskToolsServiceHeader">
                                        <a class="colmn1" style="width:70px" title="Nach Status sortieren" onclick="sortService(0)">Zustand</a>
                                        <a class="colmn2" title="Nach Name sortieren" onclick="sortService(1)">Name</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()">Senden</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="Zwischenablage" onkeypress="return false" onkeydown="return false" onclick="showDeskClip()">
                            <input id="DeskType" class="btn btn-secondary btn-sm me-1" cmenu="deskPreConfigShortcutContextMenu" type="button" value="Modus" 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="Bildschirmtastatur ein-/ausblenden" style="display:none">
                                <i class="fa-solid fa-keyboard"></i>
                            </button>
                            <div id="DeskControlSpan" title="Maus- und Tastatureingaben ein-/ausschalten" 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">Eingabe</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="Weitere Aktionen">
                                Mehr&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="Desktop-Sitzungslatenz"></span>
                            <span id="DeskTimer" style="width:70px" class="text-center" title="Sitzungszeit"></span>
                            <input id="DeskToolsButton" type="button" class="btn btn-primary btn-sm mx-1" value="Werkzeuge" title="Werkzeug-Ansicht ein-/ausschalten" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()">
                            <span id="DeskGuestShareButton" title="Teilen Sie das Gerät mit einem Gast" role="button">
                                <i class="fa-solid fa-fw fa-share-nodes" id="DeskInputUnLockedButtonImage" onclick="showShareDevice()"></i>
                            </span>
                            <span id="DeskInputUnLockedButton" title="Remote-Eingabe ist entsperrt" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" onclick="deskInputLockFunction(1)"></i>
                            </span>
                            <span id="DeskInputLockedButton" title="Remote-Eingabe ist gesperrt" role="button">
                                <i class="fa-solid fa-fw fa-keyboard" style="color:red" onclick="deskInputLockFunction(0)"></i>
                            </span>
                            <span id="DeskRefreshButton" title="Desktop aktualisieren" role="button">
                                <i class="fa-solid fa-fw fa-rotate" onclick="deskRefreshFunction()"></i>
                            </span>
                            <span id="DeskClipboardOutButton" title="Laden Sie die lokale Zwischenablage auf das Remote-Gerät hoch" 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="Die Remote-Zwischenablage in die lokale Zwischenablage herunterladen" 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="Zeichnen Sie die Remotedesktopsitzung in einer Datei auf" style="display:none" role="button">
                                <i class="fa-solid fa-fw fa-video" onclick="deskRecordSession()" id="DeskRecordButtonImage"></i>
                            </span>
                            <span id="DeskSaveImageButton" title="Ein Bildschirmfoto des entfernten Desktops speichern" role="button">
                                <i class="fa-solid fa-fw fa-camera" onclick="deskSaveImage()"></i>
                            </span>
                            <span id="DeskBackgroundButton" title="Hintergrundbild des entfernten Desktops ein-/ausschalten" role="button">
                                <i class="fa-solid fa-fw fa-image" onclick="deviceToggleBackground(event)" id="DeskBackgroundButtonImage"></i>
                            </span>
                            <span id="DeskOpenWebButton" title="Eine Web-Adresse auf dem entfernten Rechner öffnen" role="button">
                                <i class="fa-solid fa-fw fa-globe" onclick="deviceUrlFunction()"></i>
                            </span>
                            <span id="DeskLockButton" title="Sperre den Remote Computer" role="button">
                                <i class="fa-solid fa-fw fa-lock" onclick="deviceLockFunction()"></i>
                            </span>
                            <span id="DeskNotifyButton" title="Eine Benachrichtigung auf dem entfernten Rechner anzeigen" role="button">
                                <i class="fa-solid fa-fw fa-bell" onclick="deviceToastFunction()"></i>
                            </span>
                            <span id="DeskChatButton" class="deskarea" title="Chat-Fenster mit diesem Computer öffnen" role="button">
                                <i class="fa-solid fa-fw fa-message" onclick="deviceChat(event)"></i>
                            </span>
                            <span id="DeskRunButton" cmenu="deskPreConfigScriptContextMenu" class="deskarea" title="Ein Skript auf diesem Computer ausführen" 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>Monitor auswählen<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>Bildschirmtastatur</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>Text eingeben</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>Aktionen</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>Einstellungen</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>Touchpad-Modus aktivieren</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>Rechtsklick-Modus aktivieren</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>Tastenkürzel anpassen</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>Desktop aktualisieren</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>Zwischenablage</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>Zwischenablage hochladen</a>
                        <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceUrlFunction();deskCloseMobileActions()"><i class="fa-solid fa-globe fa-fw me-2 text-primary"></i>URL auf Remote öffnen</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>Remote sperren</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>Benachrichtigung senden</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>Skript ausführen</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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-terminal fa-sm me-2 text-secondary"></i>Terminal<span id="p12deviceName"></span></div>
                    <div id="devListToolbarViewIcons2" class="ms-auto">
                        <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick="deskToggleFull(event)" title="Vollbild. Halten Sie die Umschalttaste gedrückt, um das Browserfenster im Vollbild anzuzeigen."></i>
                    </div>
                </div>
                <div id="p12warning" onclick="showFeaturesDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Weiterleitungsport für KVM ist deaktiviert<span id="p12warninga">, zum Aktivieren hier klicken.</span></div>
                </div>
                <div id="p12warning2" onclick="showPowerActionDlg()">
                    <div class="icon2"></div>
                    <div class="warningbox">Entfernter Rechner ist nicht eingeschaltet, klicken Sie hier um einen Stromversorgungsbefehl abzusetzen.
                </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>
                                        Auto-Verbindung</button>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2span">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2" title="Verbinden Sie sich mit MeshAgent Remote Desktop" onclick="connectTerminal(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Verbinden</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">Dropdown umschalten</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Admin-Shell</b></a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0,event)">Admin-PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Benutzer-Shell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Benutzer-PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(1,0x10,event)">Fragen Sie die Admin-Shell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(6,0x10,event)">Fragen Sie den Administrator PowerShell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(8,0x10,event)">Fragen Sie die Benutzer-Shell</a></li>
                                            <li><a class="dropdown-item" onclick="cmtermaction(9,0x10,event)">Fragen Sie Benutzer PowerShell</a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(8,0,event)"><b>Benutzer-Shell</b></a></li>
                                            <li class="ass"><a class="dropdown-item" onclick="cmtermaction(9,0,event)">Benutzer-PowerShell</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(1,0,event)"><b>Root-Shell</b></a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(8,0,event)">Benutzer-Shell</a></li>
                                            <li class="lin"><a class="dropdown-item" onclick="cmtermaction(100,0,event)">Login Shell</a></li>
                                        </ul>
                                    </div>
                                    <div class="btn-group dropdown-center me-1" id="connectbutton2sspan">
                                        <button type="button" class="btn btn-success btn-sm" id="connectbutton2s" title="Verbinden Sie sich mit MeshAgent Remote Desktop" onclick="connectTerminal(event,3)" onkeypress="return false" onkeydown="return false" disabled="disabled">SSH verbinden</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">Dropdown umschalten</span></button>
                                        <ul class="dropdown-menu">
                                            <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Alternativer Port</a></li>
                                        </ul>
                                    </div>
                                    <span id="connectbutton2hspan"><button type="button" class="btn btn-success btn-sm me-1" id="connectbutton2h" title="Stellen Sie eine Verbindung mit Intel® AMT Hardware KVM her" onclick="connectTerminal(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">HW-Verbindung</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">Trennen</button></span>
                                    <span id="termstatus" style="line-height:22px">Getrennt</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="Server zeichnet diese Sitzung auf" 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="Energiezustandsaktionen auf dem Gerät ausführen" onkeypress="return false" onkeydown="return false" value="Aktionen" 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="Strg+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="Strg+X" onclick="termSendKey(24,'ctrlxbutton')">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" id="escbutton" value="Esc" onclick="termSendKey(27,'escbutton')">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" id="tabbutton" value="Tab" onclick="termSendKey(9,'tabbutton')" style="display:none">
                                    <input type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" id="bsbutton" value="Rücktaste" 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="Einfügen" title="Text ins Terminal einfügen" onclick="showTermPasteDialog()" style="display:none">
                                </div>
                                <div class="d-flex align-items-center ms-auto">
                                    <span id="TermTimer" title="Sitzungszeit"></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="Umschalten, was Return-Taste sendet" 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="Emulationstyp für F1 bis F10 umschalten" onclick="termToggleFx()">
                                        <input id="id_ttypebutton" type="button" onkeypress="return false" onkeydown="return false" class="btn btn-primary me-1 btn-sm" value="Erweitertes ASCII" title="Terminal-Emulationstyp umschalten" 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="Senden" title="Sende den ausgewählten Spezialschlüssel" 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="Zurück" 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>Dateien<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> Auto-Verbindung</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connect" title="Verbinden Sie sich mit MeshAgent Remote Desktop" onclick="connectFiles(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">Verbinden</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">Dropdown umschalten</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmconnectfilesaction()">Einwilligung anfordern</a></li>
                                    </ul>
                                </div>
                                <button id="p13Disconnect" class="btn btn-danger btn-sm me-1" onclick="connectFiles(event)" type="button">Trennen</button>
                                <div class="btn-group dropdown-center me-1" id="p13Connectsspan">
                                    <button type="button" class="btn btn-success btn-sm" id="p13Connects" title="Verbinden Sie sich mit MeshAgent Remote Desktop" onclick="connectFiles(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">SFTP-Verbindung</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">Dropdown umschalten</span></button>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" onclick="cmsshportaction(1,event)">Alternativer Port</a></li>
                                    </ul>
                                </div>
                                <span id="p13Status">Getrennt</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="Energiezustandsaktionen auf dem Gerät ausführen" value="Aktionen" onclick="deviceActionFunction()">
                                <div id="filesRecordIcon" class="deskareaicon" title="Server zeichnet diese Sitzung auf" 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="Hoch" onclick="p13folderup()">
                                <input type="button" disabled="disabled" id="p13SelectAllButton" class="btn btn-secondary me-1 btn-sm" value="Alle auswählen" onclick="p13selectallfile()">
                                <input type="button" disabled="disabled" id="p13RenameFileButton" class="btn btn-primary me-1 btn-sm" value="Umbenennen" onclick="p13renamefile()">
                                <input type="button" disabled="disabled" id="p13DeleteFileButton" class="btn btn-primary me-1 btn-sm" value="Löschen" onclick="p13deletefile()">
                                <input type="button" disabled="disabled" id="p13ViewFileButton" class="btn btn-primary me-1 btn-sm" value="Bearbeiten" onclick="p13viewfile()">
                                <input type="button" disabled="disabled" id="p13NewFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Neuer Ordner" onclick="p13createfolder()">
                                <input type="button" disabled="disabled" id="p13NewFileButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Neue Datei" onclick="p13createfile()">
                                <input type="button" disabled="disabled" id="p13UploadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Hochladen" onclick="p13uploadFile()">
                                <input type="button" disabled="disabled" id="p13DownloadButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Herunterladen" onclick="p13downloadButton()">
                                <input type="button" disabled="disabled" id="p13CutButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Ausschneiden" onclick="p13copyFile(1)">
                                <input type="button" disabled="disabled" id="p13CopyButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Kopie" onclick="p13copyFile(0)">
                                <input type="button" disabled="disabled" id="p13PasteButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Einfügen" onclick="p13pasteFile()">
                                <input type="button" disabled="disabled" id="p13ZipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Zip" onclick="p13zipFiles()">
                                <input type="button" disabled="disabled" id="p13UnzipButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Unzip" onclick="p13unzipFile()">
                                <input type="button" disabled="disabled" id="p13RefreshButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Aktualisieren" onclick="p13folderup(9999)">
                                <input type="button" disabled="disabled" id="p13FindButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Finden" onclick="p13findfile()">
                                <input type="button" disabled="disabled" id="p13GoToFolderButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Gehe zu" onclick="p13gotofolder()">
                                <input type="button" disabled="disabled" id="p13OpenButton" class="btn btn-primary me-1 btn-sm desktopFileAction" value="Öffnen" 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">Mehr&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()">Neuer Ordner</button>
                                        <button type="button" disabled="disabled" id="p13MobileNewFileButton" class="dropdown-item" onclick="p13createfile()">Neue Datei</button>
                                        <button type="button" disabled="disabled" id="p13MobileUploadButton" class="dropdown-item" onclick="p13uploadFile()">Hochladen</button>
                                        <button type="button" disabled="disabled" id="p13MobileDownloadButton" class="dropdown-item" onclick="p13downloadButton()">Herunterladen</button>
                                        <button type="button" disabled="disabled" id="p13MobileCutButton" class="dropdown-item" onclick="p13copyFile(1)">Ausschneiden</button>
                                        <button type="button" disabled="disabled" id="p13MobileCopyButton" class="dropdown-item" onclick="p13copyFile(0)">Kopie</button>
                                        <button type="button" disabled="disabled" id="p13MobilePasteButton" class="dropdown-item" onclick="p13pasteFile()">Einfügen</button>
                                        <button type="button" disabled="disabled" id="p13MobileZipButton" class="dropdown-item" onclick="p13zipFiles()">Zip</button>
                                        <button type="button" disabled="disabled" id="p13MobileUnzipButton" class="dropdown-item" onclick="p13unzipFile()">Unzip</button>
                                        <button type="button" disabled="disabled" id="p13MobileRefreshButton" class="dropdown-item" onclick="p13folderup(9999)">Aktualisieren</button>
                                        <button type="button" disabled="disabled" id="p13MobileFindButton" class="dropdown-item" onclick="p13findfile()">Finden</button>
                                        <button type="button" disabled="disabled" id="p13MobileGoToFolderButton" class="dropdown-item" onclick="p13gotofolder()">Gehe zu</button>
                                        <button type="button" disabled="disabled" id="p13MobileOpenButton" class="dropdown-item" onclick="p13openfilefolder()">Öffnen</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">Dynamisch</option>
                                    <option value="1" title="Bytes">Bytes</option>
                                    <option value="10" title="KB">Kilobytes</option>
                                    <option value="20" title="MB">Megabytes</option>
                                    <option value="30" title="GB">Gigabyte</option>
                                </select>
                                <select id="p13sortdropdown" class="form-select-sm me-2" onchange="p13updateFiles()">
                                    <option value="1" selected="selected">Nach Name sortieren</option>
                                    <option value="2">Nach Größe sortieren</option>
                                    <option value="3">Nach Datum sortieren</option>
                                    <option value="4">Nach Name absteigend</option>
                                    <option value="5">Nach Größe absteigend</option>
                                    <option value="6">Nach Datum absteigend</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="Zurück" 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="Vollbild. Halten Sie die Umschalttaste gedrückt, um das Browserfenster im Vollbild anzuzeigen."></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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Konsole<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="Informationen zum derzeit auf diesem Agent ausgeführten Kern"></div>
                                <input type="button" id="p15uploadCore" class="btn btn-primary me-1 btn-sm" value="Agent-Aktion" onclick="p15uploadCore(event)" title="Das JavaScript-Codemodul des Agent wechseln">
                                <i onclick="p15downloadConsoleText()" title="Konsolentext herunterladen" class="fa-solid fa-download me-1"></i>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <div class="areaProgress">
                                <div id="consoleprogressbar" style=""></div>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <td id="p15agentConsole">
                            <pre id="p15agentConsoleText"></pre>
                        </td>
                    </tr>
                    <tr>
                        <td class="areaFoot">
                            <table style="width:100%">
                                <tbody><tr>
                                    <td style="width:99%">
                                        <input id="p15consoleText" class="form-control" onkeyup="p15consoleSend(event)" onfocus="onConsoleFocus(1)" onblur="onConsoleFocus(0)">
                                    </td>
                                    <td>&nbsp;</td>
                                    <td id="p15outputselecttd">
                                        <select id="p15outputselect" onchange="setupConsole()">
                                            <option id="p15outputselect1" value="1">Agent</option>
                                            <option id="p15outputselect3" value="3">drücken</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="Leeren" 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="Zurück" 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>Ereignisse<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">Filter</label>
                                <select id="p16filterevents" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="">Alle Protokolle</option>
                                    <option notransval="1" value="agentlog">Agenten-Protokoll</option>
                                    <option notransval="1" value="relaylog">Relay-Protokoll</option>
                                    <option notransval="1" value="manual">Manuelles Protokoll</option>
                                    <option notransval="1" value="runcommands">Befehls-Protokoll</option>
                                    <option notransval="1" value="batchupload">Batch Upload Protokoll</option>
                                    <option notransval="1" value="changenode">Knotenänderungs-Protokoll</option>
                                    <option notransval="1" value="removenode">Knoten-Entfernungs-Protokoll</option>
                                </select>
                                <label class="me-1" for="p16limitdropdown">Anzeigen</label>
                                <select id="p16limitdropdown" class="form-control-sm me-2" onchange="refreshDeviceEvents()">
                                    <option notransval="1" value="60">Letzte 60</option>
                                    <option notransval="1" value="120">Letzte 120</option>
                                    <option notransval="1" value="250">Letzte 250</option>
                                    <option notransval="1" value="500">Letzte 500</option>
                                    <option notransval="1" value="1000">Letzte 1000</option>
                                </select>
                                <i onclick="p3showDownloadEventsDialog(1)" class="fa-solid fa-download" title="Ereignisse herunterladen" 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="Zurück" 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>Details<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="Aktualisieren Sie detaillierte Informationen."></i>
                        <i class="fa-solid fa-chart-line fa-xl fa-border" role="button" onclick="deskToggleCpuGraph(event)" title="CPU- und Speicherauslastung des Geräts anzeigen."></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-Grafik</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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold"><i class="fa-solid fa-cubes fa-sm me-2 text-secondary"></i>Software<span id="p18deviceName"></span></div>
                    <div id="devListToolbarViewIcons2" class="ms-auto">
                    </div>
                </div>
                <div id="p18toolbar" class="areaHead d-flex flex-wrap">
                    <div class="d-flex align-items-center">
                        <input type="button" id="p18RefreshBtn" class="btn btn-primary btn-sm me-1" value="Aktualisieren" onclick="loadInstalledApps()">
                        <input type="text" id="p18SearchInput" class="form-control-sm" placeholder="Software suchen..." style="width:250px" onkeyup="filterInstalledApps()">
                        <label style="margin-left:15px">
                            <input type="checkbox" id="p18ShowStore" onchange="toggleStoreApps()"> Store-Apps anzeigen
                        </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>Klicken Sie auf Aktualisieren, um installierte Software zu laden...</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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Plugins<span id="p19deviceName"></span></div>
                </div>
                <div id="p19headers"></div>
                <div id="p19pages"></div>
            </div>
            <div id="p20" style="display:none">
                <div id="p20main" style="overflow-y:auto">
                    <div id="p20title" class="d-flex align-items-center">
                        <div id="p20BackButton" class="pe-2">
                            <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex="0" onclick="goBack()" title="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                        </div>
                        <div class="fs-4 fw-bold">Allgemein<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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Zusammenfassung - <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">Energiezustände</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">Agent-Typen</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">Konnektivität</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">Sicherheit</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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Allgemein<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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Ereignisse<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">Filter</label>
                                <select id="p31filterevents" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="">Alle Protokolle</option>
                                    <option notransval="1" value="agentlog">Agenten-Protokoll</option>
                                    <option notransval="1" value="relaylog">Relay-Protokoll</option>
                                    <option notransval="1" value="manual">Manuelles Protokoll</option>
                                    <option notransval="1" value="runcommands">Befehls-Protokoll</option>
                                    <option notransval="1" value="batchupload">Batch Upload Protokoll</option>
                                    <option notransval="1" value="changenode">Knotenänderungs-Protokoll</option>
                                    <option notransval="1" value="removenode">Knoten-Entfernungs-Protokoll</option>
                                </select>
                                <label for="p31limitdropdown" class="me-1">Anzeigen</label>
                                <select id="p31limitdropdown" class="form-select-sm me-2" onchange="refreshUsersEvents()">
                                    <option notransval="1" value="60">Letzte 60</option>
                                    <option notransval="1" value="120">Letzte 120</option>
                                    <option notransval="1" value="250">Letzte 250</option>
                                    <option notransval="1" value="500">Letzte 500</option>
                                    <option notransval="1" value="1000">Letzte 1000</option>
                                    <option notransval="1" value="">Keine Begrenzung</option>
                                </select>
                                <a href="#" onclick="p3showDownloadEventsDialog(3)"><i class="fa-solid fa-download" role="button" title="Ereignisse herunterladen"></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">Meine Server-Statistiken</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Aktualisieren" 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">Logarithmische Skala</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">Verbindungen</option>
                            <option value="1">Speicher</option>
                            <option value="5">CPU</option>
                            <option value="3">Eingehender Datenverkehr</option>
                            <option value="4">Ausgehender Datenverkehr</option>
                        </select>
                        <select id="p40time" class="form-select-sm me-1" onchange="updateServerTimelineHours()">
                            <option value="3">Letzte 3 Stunden</option>
                            <option value="8">Letzte 8 Stunden</option>
                            <option value="24">Letzter Tag</option>
                            <option value="168">Letzte Woche</option>
                            <option value="720">Letzte 30 Tage</option>
                        </select>
                        <i class="fa-solid fa-download" title="Datenpunkte herunterladen (.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">Mein Server-Tracing</div>
                </div>
                <div class="areaHead d-flex align-items-center flex-wrap p-1">
                    <div class="d-flex align-items-center">
                        <input value="Tracing" type="button" class="btn btn-primary btn-sm me-1" onclick="setServerTracing()">
                        <span id="p41traceStatus">Kein(e)</span>
                    </div>
                    <div class="toright2 d-flex align-items-center ms-auto">
                        <label for="p41limitdropdown" class="me-1">Anzeigen</label>
                        <select id="p41limitdropdown" class="form-select-sm me-1" onchange="displayServerTrace()">
                            <option value="100">Letzte 100</option>
                            <option value="250">Letzte 250</option>
                            <option value="500">Letzte 500</option>
                            <option value="1000">Letzte 1000</option>
                        </select>
                        <input value="Leeren" type="button" class="btn btn-primary btn-sm me-1" onclick="clearServerTracing()">
                        <i class="fa-solid fa-download" title="Trace herunterladen (.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">Meine Server-Plugins</div>
                </div>
                <div class="areaHead">
                    <div class="toright2">
                    </div>
                    <div>
                        <input value="Plugin herunterladen" type="button" onclick="return pluginHandler.addPluginDlg();">
                    </div>
                </div>
                <div id="pluginRestartNotice" class="areaHead" style="background-color:gold;display:none">
                    <div class="toright2">
                        <input value="Agent-Cores aktualisieren" type="button" onclick="distributeCore();return false">
                    </div>
                    <div style="padding:2px">
                        <div style="padding:2px"><b>Hinweis:</b> Plugins wurden geändert. Dies erfordert möglicherweise eine Aktualisierung der Agent-Cores.</div>
                    </div>
                </div>
                <table id="p42tbl">
                    <tbody><tr class="DevSt">
                        <th style="width:26px"></th>
                        <th style="width:10px"></th>
                        <th class="chName">Name</th>
                        <th class="chDescription">Beschreibung</th>
                        <th class="chSite" style="text-align:center">Link</th>
                        <th class="chVersion" style="text-align:center">Version</th>
                        <th class="chUpgradeAvail" style="text-align:center">Neueste</th>
                        <th class="chStatus" style="text-align:center">Status</th>
                        <th class="chAction" style="text-align:center">Aktion</th>
                        <th style="width:10px"></th>
                    </tr>
                </tbody></table>
                <div id="pluginNoneNotice" style="width:100%;text-align:center;padding-top:10px;display:none"><i>Keine Plugins auf dem Server.</i></div>
            </div>
            <div id="p43" style="display:none">
                <div id="p43BackButton">
                    <div class="backButton" tabindex="0" onmouseup="go(42)" title="Zurück" onkeypress="if (event.key == 'Enter') go(42)">
                        <div class="backButtonEx"></div>
                    </div>
                </div>
                <h1>Meine Server-Plugins - <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">Meine Benutzergruppen</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="Alle auswählen">
                                <input type="button" id="UsersGroupsGroupActionButton" class="btn btn-primary btn-sm me-1" disabled="disabled" value="Gruppenaktion" onclick="p50usersGroupActionFunction()">
                                <input id="NewUserGroupButton" type="button" class="btn btn-success btn-sm me-1" onclick="showCreateUserGroupDialog(1)" value="Neue Gruppe...">
                                <input id="DuplicateUserGroupButton" type="button" class="btn btn-primary button-outline-success btn-sm me-1" style="display:none" onclick="showCreateUserGroupDialog(2)" value="Dupliziere Gruppe...">
                            </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="Zurück" onkeypress="if (event.key == 'Enter') goBack()"></i>
                    </div>
                    <div class="fs-4 fw-bold">Benutzergruppe - <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">Meine Benutzeraufzeichnungen</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="Aktualisieren">
                            </div>
                            <div class="d-flex align-items-center ms-auto">
                                <input type="button" class="btn btn-primary btn-sm" onclick="openRecodringPlayer()" value="Player öffnen...">
                            </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">Meine Berichte</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="Bericht herunterladen"></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> Bericht generieren...</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()">E-Mail verifizieren</a>
                        &nbsp;<a id="termsLinkFooter" href="terms">Nutzungsbedingungen &amp; Datenschutz</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>Dateiauswahl</div>
                                <select id="d3uploadMode" onchange="d3modechange()">
                                    <option value="1">Lokale Datei hochladen</option>
                                    <option value="2">Server-Dateiauswahl</option>
                                </select>
                            </div>
                            <div id="d3localmode" style="display:none">
                                <div>Datei hochladen</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="Hoch">&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="Umbruch an" onclick="d4ToggleWrap()">
                            <input id="d4SizeButton" class="btn btn-primary me-1 mb-1" type="button" value="Klein" onclick="d4ToggleSize()">
                            <input id="d4EncodingButton" class="btn btn-primary me-1 mb-1" type="button" value="Roh" onclick="d4ToggleEncoding()">
                            <input id="d4LineBreakButton" class="btn btn-primary me-1 mb-1" type="button" value="Windows" onclick="d4ToggleLineBreak()">
                            <textarea id="d4editorarea" autocomplete="off" style="height:calc(100vh - 286px);width:100%;overflow:scroll;resize:none;white-space:pre"></textarea>
                        </div>
                        <div id="dialog7" class="tab-content" style="">
                            <div class="nav nav-pills mb-3">
                                <button id="td7meshkvm" class="nav-link" onclick="changeDesktopSettingsTab(event, 'd7meshkvm')">Agent</button>
                                <button id="td7rdpkvm" class="nav-link" onclick="changeDesktopSettingsTab(event, 'd7rdpkvm')">RDP</button>
                                <button id="td7amtkvm" class="nav-link" onclick="changeDesktopSettingsTab(event, 'd7amtkvm')">Intel® AMT</button>
                            </div>
                            <div id="d7meshkvm" class="tab-pane">
                                <!--<h4>Agent Remote Desktop</h4>-->
                                <div class="form-floating mb-2">
                                    <select id="d7bitmapquality" class="form-select"></select>
                                    <label for="d7bitmapquality">Qualität</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">Skalierung</label>
                                </div>
                                <div class="form-floating mb-2">
                                    <select id="d7framelimiter" class="form-select">
                                        <option selected="selected" value="50">Schnell</option>
                                        <option value="100">Mittel</option>
                                        <option value="400">Langsam</option>
                                        <option value="1000">Sehr langsam</option>
                                    </select>
                                    <label for="d7framelimiter">Bildrate</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">Kodierung</label>
                                </div>
                                <fieldset id="d7desktopOtherSettings">
                                    <legend>Weitere Einstellungen</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">Maustasten vertauschen</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskrmw" class="form-check-label">Umgekehrtes Mausrad</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7deskRemoteKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7deskRemoteKeyMap" class="form-check-label">Entfernte Tastaturbelegung verwenden</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">Automatische Zwischenablage</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">Sperren bei Trennen</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, am schnellsten</option>
                                        <option value="2">RLE16, empfohlen</option>
                                        <option value="3">RAW8, langsam</option>
                                        <option value="4">RAW16, sehr langsam</option>
                                    </select>
                                    <label for="d7desktopmode">Bildkodierung</label>
                                </div>
                                <fieldset>
                                    <legend>Weitere Einstellungen</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">Fokus-Werkzeug anzeigen</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7showcursor" class="form-check-input me-2" type="checkbox">
                                            <label for="d7showcursor" class="form-check-label">Lokalen Mauszeiger anzeigen</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7localKeyMap" class="form-check-input me-2" type="checkbox">
                                            <label for="d7localKeyMap" class="form-check-label">Lokale Tastaturbelegung</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7kvmrmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7kvmrmw" class="form-check-label">Umgekehrtes Mausrad</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">Leinwandgröße</option>
                                        <option notransval="1" value="browser">Browsergröße</option>
                                        <option notransval="1" value="screen">Bildschirmgröße</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">Bildschirmgröße</label>
                                </div>
                                <fieldset>
                                    <legend>Optionen</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">Deaktiviere Hintergrundbild</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp2" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp2" class="form-check-label">Deaktivieren Sie das Ziehen des gesamten Fensters</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp3" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp3" class="form-check-label">Deaktiviere Menüanimationen</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp4" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp4" class="form-check-label">Design deaktivieren</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp6" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp6" class="form-check-label">Deaktiviere Cursor Schatten</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp7" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp7" class="form-check-label">Deaktiviere Cursor Einstellungen</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp8" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp8" class="form-check-label">Schriftartglättung aktivieren</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdp9" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdp9" class="form-check-label">Desktop-Gestaltung aktivieren</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpclip" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpclip" class="form-check-label">Automatische Zwischenablage</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdpsmb" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdpsmb" class="form-check-label">Maustasten vertauschen</label>
                                        </div>
                                        <div class="form-check">
                                            <input id="d7rdprmw" class="form-check-input me-2" type="checkbox">
                                            <label for="d7rdprmw" class="form-check-label">Umgekehrtes Mausrad</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">Abbrechen</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">Löschen</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 = ['', "eingeschaltet", "Ruhezustand", "Ruhezustand", "Ruhezustand", "Ruhezustand", "Ausschalten", "präsent"];
        var StatusStrs = ["Getrennt", "Verbinden...", "Einrichtung...", "Verbunden", "Intel&reg; AMT verbunden"];
        var agentsStr = ["Unbekannt", "Windows 32bit Konsole", "Windows 64bit Konsole", "Windows 32bit Dienst", "Windows 64bit Dienst", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86-32bit", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86-32bit", "macOS x86-64bit", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Windows MinCore Konsole", "Windows MinCore Dienst", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bit", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Apple Silicon", "FreeBSD x86-64", "Unbekannt", "Linux ARM 64 bit", "Alpine Linux x86 64 Bit (MUSL)", "Assistent (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Unbekannt", "Unbekannt", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Windows ARM (64bit) Konsole", "Windows ARM (64bit) Dienst", "ARMVIRT32 (OpenWRT)", "RISC-V 64bit"];
        var agentsStrNoAgent = ['', '', '', '', "Windows", '', "Linux", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', "Apple"];
        var domainStates = ["Kein(e)", "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 + '\'>' + "Strg" + '-' + 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]|Tippen");

            // 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>' + "Browser Standard" + '</label><br>';
            x += '<input type=radio id=night2 name=nightmoderadio value=2 ' + ((cNightMode == 2) ? 'checked' : '') + '><label for=night2>' + "Heller Modus" + '</label><br>';
            x += '<input type=radio id=night1 name=nightmoderadio value=1 ' + ((cNightMode == 1) ? 'checked' : '') + '><label for=night1>' + "Dunklen Modus" + '</label><br>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Nacht Modus", 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', "Benutzeroberfläche", 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', "Authentifizierung kann nicht durchgeführt werden"); return; }
                if (errorCode == 'invalidorigin') { QH('p0span', "Invalid origin in HTTP request"); return; }
                if (prevState == 2) { if (autoReconnect) { setTimeout(serverPoll, 5000); } } else { QH('p0span', "Web-Socket kann nicht verbunden werden"); }
                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: "Bitte fügen Sie Zweifaktor-Sicherungscodes hinzu. Falls der aktuelle Faktor verloren geht, kann dieses Konto nicht wiederhergestellt werden.", title: "Zweifaktor-Authentifizierung", 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', " - Beim nächsten Anmelden zurücksetzen."); }
                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', " - Beim nächsten Anmelden zurücksetzen."); }
                    else if (seconds < 3600) { var secs = Math.floor(seconds / 60); QH('p2nextPasswordUpdateTime', format((secs == 1) ? " - Zurücksetzen in 1 Minute." : " - Zurücksetzen in {0} Minuten.", secs)); }
                    else if (seconds < 86400) { var hours = Math.floor(seconds / 3600); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Zurücksetzen in 1 Stunde." : " - Zurücksetzen in {0} Stunden.", hours)); }
                    else { var days = Math.floor(seconds / 86400); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - Zurücksetzen in 1 Tag." : " - Zurücksetzen in {0} Tagen.", 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 Sekunde bis zum Abmelden" : "{0} Sekunden bis zum Abmelden")
                        : (ds == 1 ? "1 Sekunde bis zur Trennung" : "{0} Sekunden bis zur Trennung");
                } else {
                    ds = Math.round(ds / 60);
                    if (ds <= 5) {
                        theText = isLogout
                            ? (ds == 1 ? "1 Minute bis zum Abmelden" : "{0} Minuten bis zum Abmelden")
                            : (ds == 1 ? "1 Minute bis zur Trennung" : "{0} Minuten bis zur Trennung");
                    }
                }
                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', "Kein(e)");
                        }
                    }
                    break;
                }
                case 'serverstats': {
                    updateGeneralServerStats(message);
                    break;
                }
                case 'serverwarnings': {
                    if ((message.warnings != null) && (message.warnings.length > 0)) {
                        var ServerWarnings = {
                            1: "",
                            2: "Fehlende WebDAV-Parameter.",
                            3: "Unbekannte Konfigurationsoption \"{0}\".",
                            4: "Die WebSocket-Komprimierung ist deaktiviert, diese Funktion ist in NodeJS v11.11 bis v12.15 und v13.2 defekt",
                            5: "Konnte Intel AMT TLS root Zertifikat für Standarddomain nicht laden.",
                            6: "Konnte Intel AMT TLS root Zertifikat für domain {0} nicht laden.",
                            7: "Lokale CIRA-FQDNs werden ignoriert, wenn sich der Server im Nur-LAN- oder Nur-WAN-Modus befindet.",
                            8: "Kann nicht mehr als 4 lokale CIRA-FQDNs haben. Wert ignorieren.",
                            9: "Die Agent-Hash-Überprüfung wird übersprungen, dies ist unsicher.",
                            10: "Fehlende E-Mail-Adresse von Let's Encrypt.",
                            11: "Ungültige Let's Encrypt-Hostnamen.",
                            12: "Ungültige Let's Encrypt-Namen, darf kein * enthalten.",
                            13: "Let's Encrypt Modul einrichtung nicht möglich.",
                            14: "Ungültige Let's Encrypt-Namen, nicht auflösbar: {0}",
                            15: "Ungültige Let's Encrypt-E-Mail-Adresse, kann nicht aufgelöst werden: {0}",
                            16: "Liste der vertrauenswürdigen Proxy-IPv6-Adressen von CloudFlare kann nicht geladen werden.",
                            17: "Der SendGrid-Server kann im LAN-Modus nur eingeschränkt verwendet werden.",
                            18: "Der SMTP-Server kann im LAN-Modus nur eingeschränkt verwendet werden.",
                            19: "Das SMS-Gateway kann im LAN-Modus nur eingeschränkt verwendet werden.",
                            20: "Ungültiger \"LoginCookieEncryptionKey\" in config.json.",
                            21: "Backup-Pfad kann nicht innerhalb des Meshcentral-Data-Ordners festgelegt werden, Backup-Einstellungen werden ignoriert.",
                            22: "Agent {0} konnte nicht signiert werden: {1}",
                            23: "Konnte die Icon-Datei für den Agenten nicht laden: {0}.",
                            24: "Konnte die Logo-Datei für den Agenten nicht laden: {0}.",
                            25: "Diese NodeJS Version unterstützt OpenID nicht.",
                            26: "Diese NodeJS Version unterstützt Discord.js nicht.",
                            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>' + "WARNUNG: " + 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>' + "WARNUNG: " + 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>' + "WARNUNG: " + format("Zertifikat läuft in {0} Tag(en) ab", days) + '</b></div>');
                            QV('serverWarningsDiv', true);
                            addNotification({ text: format("Zertifikat läuft in {0} Tag(en) ab", 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>' + "FEHLER:" + JSON.parse(message.value).error + '</span>');
                            QH('p18html', '<div style="text-align:center;padding:50px;color:gray"><i>' + "Klicken Sie auf Aktualisieren, um installierte Software zu laden..." + '</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>' + "Zuletzt gesehen:" + '<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>' + "Erfolg" + '</span>' : '<span style=color:red>' + "Fehlgeschlagen" + '</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>' + "Vorgangsname" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.processName + '</b></div></div>'; }
                                        if (message.value.machineName) { x += addHtmlValue5("Maschinenname", message.value.machineName); }
                                        if (message.value.cmd) { x += '<div style=padding:4px><div style=padding-bottom:4px>' + "Befehlszeile" + '</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>' + "Fenstertitel" + '</div><div style=word-wrap:break-word;padding-left:6px><b>' + message.value.mainWindowTitle + '</b></div></div>'; }
                                        if (message.value.processUser) { x += addHtmlValue5("Benutzer", message.value.processUser); }
                                        if (message.value.processDomain) { x += addHtmlValue5("Domain", message.value.processDomain); }
                                        if (message.value.startTime) { x += addHtmlValue5("Startzeit", message.value.startTime); }
                                        x += '<hr />';
                                        if (message.value.PriorityBoostEnabled) { x += addHtmlValue5("Prioritäts-Boost", message.value.PriorityBoostEnabled ? "aktiviert" : "Deaktiviert"); }
                                        if (message.value.sessionId) { x += addHtmlValue5("Sitzungs-ID", message.value.sessionId); }
                                        if (message.value.handleCount) { x += addHtmlValue5("Handle-Anzahl", message.value.handleCount); }
                                        if (message.value.privilegedProcessorTime) { x += addHtmlValue5("Privilegierte Prozessorzeit", format("{0} Sekunden", message.value.privilegedProcessorTime)); }
                                        if (message.value.totalProcessorTime) { x += addHtmlValue5("Gesamtprozessorzeit", format("{0} Sekunden", message.value.totalProcessorTime)); }
                                        if (message.value.userProcessorTime) { x += addHtmlValue5("Benutzerprozessorzeit", format("{0} Sekunden", message.value.userProcessorTime)); }
                                        if (message.value.nonpagedSystemMemorySize) { x += addHtmlValue5("Nicht ausgelagerter Speicher", getNiceSize2(message.value.nonpagedSystemMemorySize)); }
                                        if (message.value.pagedMemorySize) { x += addHtmlValue5("Ausgelagerter Speicher", getNiceSize2(message.value.pagedMemorySize)); }
                                        if (message.value.privateMemorySize) { x += addHtmlValue5("Privater Speicher", getNiceSize2(message.value.privateMemorySize)); }
                                        if (message.value.virtualMemorySize) { x += addHtmlValue5("Virtueller Speicher", getNiceSize2(message.value.virtualMemorySize)); }
                                        if (message.value.workingSet) { x += addHtmlValue5("Workingset", getNiceSize2(message.value.workingSet)); }
                                        if (message.value.peakWorkingSet) { x += addHtmlValue5("Spitzenarbeitssatz", getNiceSize2(message.value.peakWorkingSet)); }
                                        if (message.value.peakPagedMemorySize) { x += addHtmlValue5("Auslagerungsspeicher mit Spitzenwert", getNiceSize2(message.value.peakPagedMemorySize)); }
                                        if (message.value.peakVirtualMemorySize) { x += addHtmlValue5("Spitze des virtuellen Speichers", getNiceSize2(message.value.peakVirtualMemorySize)); }
                                    } else {
                                        x += "Keine Angaben gemacht";
                                    }
                                    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("Erste Agentenverbindung", printDateTime(new Date(currentNode.firstconnect))); }
                        if (currentNode.lastconnect) { x += addHtmlValue2("Letzte Agent-Verbindung", printDateTime(new Date(currentNode.lastconnect))); }
                        if (currentNode.lastaddr) {
                            var splitip = currentNode.lastaddr.split(':');
                            if (splitip.length > 2) {
                                // IPv6
                                x += addHtmlValue2("Letzte Agent-Adresse", currentNode.lastaddr + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(currentNode.lastaddr) + '")></i>');
                            } else {
                                // IPv4
                                if (isPrivateIP(currentNode.lastaddr)) {
                                    x += addHtmlValue2("Letzte Agent-Adresse", splitip[0] + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                } else {
                                    x += addHtmlValue2("Letzte Agent-Adresse", '<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="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(splitip[0]) + '")></i>');
                                }
                            }
                        }
                        if (message.updateTime != null) { x += addHtmlValue2("Letzte Interface-Aktualisierung", 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("Name", '<b>' + EscapeHtml(net.name) + '</b>'); }
                                if (net.desc) { x += addHtmlValue2("Beschreibung", EscapeHtml(net.desc).replace('(R)', '&reg;').replace('(r)', '&reg;')); }
                                if (net.dnssuffix) { x += addHtmlValue2("DNS-Suffix", EscapeHtml(net.dnssuffix) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Name in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.dnssuffix) + '")></i>'); }
                                if (net.mac) { x += addHtmlValue2("MAC-Adresse", '<a href="https://maclookup.app/search/result?mac=' + net.mac.substring(0, 6) + '" rel="noreferrer noopener" target="MeshMACLoopup">' + EscapeHtml(net.mac.toLowerCase()) + '</a> <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "MAC-Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.mac.toLowerCase()) + '")></i>'); }
                                if (net.v4addr) { x += addHtmlValue2("IPv4-Adresse", EscapeHtml(net.v4addr) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4addr) + '")></i>'); }
                                if (net.v4mask) { x += addHtmlValue2("IPv4-Netzmaske", EscapeHtml(net.v4mask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4mask) + '")></i>'); }
                                if (net.v4gateway) { x += addHtmlValue2("IPv4-Gateway", EscapeHtml(net.v4gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(net.v4gateway) + '")></i>'); }
                                if (net.gatewaymac) { x += addHtmlValue2("Gateway-MAC", '<a href="https://maclookup.app/search/result?mac=' + net.gatewaymac.substring(0, 6) + '" rel="noreferrer noopener" target="MeshMACLoopup">' + EscapeHtml(net.gatewaymac.toLowerCase()) + '</a> <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "MAC-Adresse in Zwischenablage kopieren" + '" 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("Name", '<b>' + EscapeHtml(i) + '</b>');
                                if (typeof net[0].mac == 'string') { x += addHtmlValue2("MAC-Adresse", '<a href="https://maclookup.app/search/result?mac=' + net[0].mac.split(':').join('').substring(0, 6) + '" rel="noreferrer noopener" target="MeshMACLoopup">' + EscapeHtml(net[0].mac.toLowerCase()) + '</a> <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "MAC-Adresse in Zwischenablage kopieren" + '" 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-Adresse", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("IPv6-Maske", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("IPv6-Gateway", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    } else if (netif.family == 'IPv4') {
                                        if (netif.address) { x += addHtmlValue2("IPv4-Adresse", EscapeHtml(netif.address) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.address) + '")></i>'); }
                                        if (netif.netmask) { x += addHtmlValue2("IPv4-Netzmaske", EscapeHtml(netif.netmask) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.netmask) + '")></i>'); }
                                        if (netif.gateway) { x += addHtmlValue2("IPv4-Gateway", EscapeHtml(netif.gateway) + ' <i class="fa-fw fa-regular fa-clipboard fa-sm" role=button title="' + "Adresse in Zwischenablage kopieren" + '" onclick=copyTextToClip2("' + encodeURIComponentEx(netif.gateway) + '")></i>'); }
                                    }
                                }
                            }
                        } else {
                            x += '<hr />'
                            x += "Keine Netzwerkschnittstelleninformationen für dieses Gerät verfügbar.";
                        }
                        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 = "Unbekannt"; }
                        if (!message.tags.stable) { message.tags.stable = "Unbekannt"; }
                        if (!message.tags.latest) { message.tags.latest = "Unbekannt"; }
                        x += addHtmlValue2("Aktuelle Version", '<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 == "Unbekannt") || (message.tags.current == message.tags.stable) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Stabile Version" + '</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 == "Unbekannt") || (message.tags.current == message.tags.latest) || ((features & 2048) == 0)) ? ' disabled' : '') + ' /> ' + "Neueste Version" + '</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-Version", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        } else {
                            x += '<hr />' + "Haken setzen und OK klicken, um die Server-Selbstaktualisierung zu starten.";
                            xxdialogTag = message.tags;
                            setModalContent('xxAddAgent', "MeshCentral-Version", 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', "Serverfehler", "Server besitzt kein Fehlerprotokoll.");
                            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="' + "Fehlerprotokoll herunterladen" + '" onclick=d2CopyServerErrorsToClip()></i></div>';
                            x += '<div><label><input id=d2clearErrorsCheck type=checkbox class="form-check-input me-2" onclick=server_showErrorsDlgUpdate() /> ' + "Haken setzen und OK klicken, um das Fehlerprotokoll zu löschen." + '</label></div>';
                            setModalContent('xxAddAgent', "MeshCentral-Serverfehler", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', server_showErrorsDlgEx);
                            server_showErrorsDlgUpdate();
                        }
                    }
                    break;
                }
                case 'serverconfig': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'MeshCentralServerConfig')) {
                        if (message.data == null) {
                            setModalContent('xxAddAgent', "Serverkonfiguration", 'Server has no config file.', 'extra-large');
                        } else {
                            setModalContent('xxAddAgent', "Serverkonfiguration", 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 ist gesperrt", "Sicherungscodes sind gesperrt", "Login-Token verwendet", "OTP 2FA nicht erlaubt", "Konto ist gesperrt", "OTPLIB konnte nicht geladen werden"];
                            if ((message.err > 0) && (message.err < otpauthErrors.length)) { QH('d2optinfo', otpauthErrors[message.err]); } else { QH('d2optinfo', format("Fehler #{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("Installieren" + ' <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" rel="noreferrer noopener" target=_blank>' + "Google Authenticator" + '</a> ' + "oder eine kompatible Applikation und scanne den Barcode, verwende <a href=\"{0}\" rel=\"noreferrer noopener\" target=_blank>diesen Link</a> oder gebe das Geheimnis manuell unten ein. Anschließend gebe den aktuellen 6-stelligen Token nachfolgend ein, um die Zweifaktor-Anmeldung zu aktivieren.", message.url) + '<br /><br />' + 'Secret <img src=images/link4.png height=10 width=10 title="' + "Secret in Zwischenablage kopieren" + '" 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 />' + "Geben Sie hier den Token für Zweifaktor-Anmeldung ein:" + ' <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', "Authentifikator-Anwendung", message.success ? ('<b style=color:green>' + "Aktivierung der Authentifikator-Anwendung erfolgreich." + '</b> ' + "Sie benötigen jetzt ein gültiges Token, um sich erneut anzumelden.") : ('<b style=color:red>' + "Aktivierung der Zweifaktor-Anmeldung fehlgeschlagen." + '</b> ' + "Löschen Sie das Geheimnis aus der Anwendung und versuchen Sie es erneut.Sie haben nur wenige Minuten Zeit, um den richtigen Code einzugeben."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-clear': {
                    if (xxdialogMode) return;
                    setModalContent('xxAddAgent', "Authentifikator-Anwendung", message.success ? ('<b>' + "Authentifikator-Anwendung entfernt." + '</b> ' + "Sie können diese Funktion jederzeit wieder aktivieren.") : ('<b style=color:red>' + "Entfernung der Aktivierung der Zweifaktor-Anmeldung fehlgeschlagen." + '</b> ' + "Versuchen Sie es erneut."));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'otpauth-getpasswords': {
                    if (xxdialogMode == 2 && (xxdialogTag != 'otpauth-manage')) return;
                    var x = "Einmal-Token können als sekundäre Authentifizierung verwendet werden. Erstellen Sie ein Set, drucken Sie es aus und bewahren Sie es an einem sicheren Ort auf.";
                    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>' + "Keine aktiven Token";
                    }
                    x += '</table></div></div><br />';
                    x += '<div class="btn-group">';
                    x += '<button type=button class="btn btn-warning" onclick="account_manageOtp(1);">' + "Neue Token generieren" + '</input>';
                    if (message.passwords != null) {
                        x += '<button type=button class="btn btn-danger" onclick="account_manageOtp(2);">' + "Token löschen" + '</button></div>';
                        x += '&nbsp;<i class="fa-regular fa-clipboard" title="' +  "Gültige Codes in Zwischenablage kopieren" + '" style="cursor:pointer" onclick=copyTextToClip2("' + encodeURIComponentEx(clipb) + '")></i>';
                    } else {
                        x += '</div>'
                    }
                    xxdialogTag = 'otpauth-manage';
                    setModalContent('xxAddAgent', "Sicherungscodes verwalten", 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">' + "Hardware-Schlüssel" + '</a> ' + "werden für Zweifaktor-Anmeldung verwendet.";
                    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 + ')>' + "Entfernen" + '</button>' + end;
                        }
                    } else {
                        x += start + '<tr style=text-align:center><td>' + "Keine Schlüssel konfiguriert" + 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);">' + "Schlüssel hinzufügen" + '</input>'; }
                        if ((features & 0x00004000) != 0) { x += '<button class="btn btn-primary" id=d2addkey2 type=button onclick="account_addhkey(2);">' + "YubiKey&reg; OTP hinzufügen" +'</input>'; }
                    } else {
                        x += "Maximale Schlüsselanzahl erreicht.";
                    }
                    x += '</div><br />';
                    xxdialogTag = 'otpauth-hardware-manage';
                    setModalContent('xxAddAgent', "Sicherheitsschlüssel verwalten", 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', "Sicherheitsschlüssel hinzufügen", '<br />' + "Fehler, Schlüssel kann nicht hinzugefügt werden." + '<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', "Sicherheitsschlüssel hinzufügen", '<br />' + "Fehler, Schlüssel kann nicht hinzugefügt werden." + '<br /><br />');
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');

                    }
                    break;
                }
                case 'webauthn-startregister': {
                    if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
                    var x = "Drücken Sie jetzt die Schlüsseltaste." + '<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', "Sicherheitsschlüssel hinzufügen", 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("FEHLER:" + error);
                        setModalContent('xxAddAgent', "Sicherheitsschlüssel hinzufügen", "FEHLER:" + 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>' + "Überprüfen Sie Ihr Telefon und geben Sie den Bestätigungscode ein.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Bestätigungscode:" + ' <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', "Telefonische Benachrichtigungen", 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>' + "Prüfen Sie Ihre Messenger-App und geben Sie den Verifikationscode ein.";
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "Bestätigungscode:" + ' <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("Hilfe angefordert von {0}: {1}", message.event.msgArgs[0], message.event.msgArgs[1]), title: n.name, icon: n.icon, nodeid: message.event.nodeid });
                                }
                            }
                            break;
                        }
                        case 'recording': {
                            if ((p52recordings != null) && (typeof p52recordings == 'object')) { p52recordings.unshift(message.event); message.event.present = 1; updateRecordings(); }
                            break;
                        }
                        case 'userWebState': {
                            // New user web state, update the web page as needed
                            try {
                                if (localStorage != null) {
                                    // TODO: The problem with this "old" values is that if changed from a different tab, the old and new values will be the same.
                                    // So comparing against these values is not a good idea.
                                    var oldShowRealNames = localStorage.getItem('showRealNames');
                                    var oldUiMode = localStorage.getItem('uiMode');
                                    var oldSort = localStorage.getItem('sort');
                                    var oldLoctag = localStorage.getItem('loctag');
                                    var oldNightMode = localStorage.getItem('nightMode');
                                    var oldFooterBar = localStorage.getItem('footerBar');

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                // Show the notification
                                if (n & 2) {
                                    var agentPrivilages = "Agent verbunden";
                                    if ((node.agent != null) && (node.agent.root === false)) { agentPrivilages = "Agent mit eingeschränkten Privilegien verbunden"; }
                                    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 verbunden"; addNotification(abc); }
                                    if (((node.conn & 4) == 0) && ((message.event.conn & 4) != 0)) { abc.text = "Intel AMT verbunden"; addNotification(abc); }
                                    if (((node.conn & 16) == 0) && ((message.event.conn & 16) != 0)) { abc.text = "MQTT verbunden"; addNotification(abc); }
                                }
                                if (n & 4) {
                                    var abc = { title: node.name, icon: node.icon, nodeid: node._id, id: message.event.id };
                                    if (((node.conn & 1) != 0) && ((message.event.conn & 1) == 0)) { abc.text = "Agent getrennt"; addNotification(abc); }
                                    if (((node.conn & 2) != 0) && ((message.event.conn & 2) == 0)) { abc.text = "Intel AMT CIRA getrennt"; addNotification(abc); }
                                    if (((node.conn & 4) != 0) && ((message.event.conn & 4) == 0)) { abc.text = "Intel AMT getrennt"; addNotification(abc); }
                                    if (((node.conn & 16) != 0) && ((message.event.conn & 16) == 0)) { abc.text = "MQTT getrennt"; 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>' + "Dieser Adressbereich kann nicht gescannt werden." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Beispiel für IP-Bereichswerte" + '<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 += " mit TLS."; } else { str += " ohne TLS."; } } else { str += ' not activated.'; }
                                    x += '<div style=width:100%;margin-bottom:2px;background-color:lightgray><div style=padding:4px><div style=display:inline-block;margin-right:5px><input class="DevScanCheckbox form-check-input me-2" name=dp1checkbox tag="' + EscapeHtml(i) + '" type=checkbox onclick=addAmtScanToMeshCheckbox() /></div><div class=j1 style=display:inline-block></div><div style=display:inline-block;margin-left:5px;overflow-x:auto;white-space:nowrap>' + str + '</div></div></div>';
                                }
                                // If no results where found, display a nice message
                                if (x == '') { x = '<div style=width:100%;text-align:center;margin-top:12px>' + "Scan ergab keine Ergebnisse." + '</div><div style=width:100%;text-align:center;margin-top:12px;color:gray;line-height:1.5>' + "Beispiel für IP-Bereichswerte" + '<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', "Kein(e)");
                                }
                            }
                            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 konnte keine Ereignismeldung anzeigen:", 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("Berechtigungen erfolgreich gespeichert");
                    } else {
                        alert("Fehler beim Speichern der Berechtigungen: " + 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 Stunde" : "{0} Stunden", message.expire);
                    if (message.expire == 24) { t = "1 Tag"; }
                    if (message.expire == 168) { t = "1 Woche"; }
                    if (message.expire == 5040) { t = "1 Monat"; }
                    if (message.expire == 0) { t = "Unbegrenzt"; }
                    QH('agentInvitationLink', format("Einladungslink ({0})", t));
                    QV('agentInvitationLinkDiv', true);
                    break;
                }
                case 'createDeviceShareLink': { // Guest sharing link
                    var node = getNodeFromId(message.nodeid), x = '';
                    if (node == null) break;
                    x += addHtmlValue("Gerät", EscapeHtml(node.name));
                    x += addHtmlValue("Gastname", message.guestname);
                    x += addHtmlValue("Benutzereingabe", message.viewOnly ? "Nicht erlaubt, nur ansehen" : "Erlaubt");
                    if (message.start && message.expire) {
                        if (message.recurring) {
                            x += addHtmlValue("Startzeit", printDateTime(new Date(message.start)));
                            if (message.expire < 2) { x += addHtmlValue("Dauer", format("{0} Minute", message.expire)); }
                            else { x += addHtmlValue("Dauer", format("{0} Minuten", message.expire)); }
                        } else {
                            x += addHtmlValue("Startzeit", printDateTime(new Date(message.start)));
                            x += addHtmlValue("Ablaufzeit", printDateTime(new Date(message.expire)));
                        }
                    }
                    if (message.recurring == 1) { x += addHtmlValue("Wiederkehrend", "Täglich"); }
                    if (message.recurring == 2) { x += addHtmlValue("Wiederkehrend", "Wöchentlich"); }
                    var y = [];
                    if (message.consent & 0x0007) { y.push("Benachrichtigen"); }
                    if (message.consent & 0x0038) { y.push("Prompt"); }
                    if (message.consent & 0x0040) { y.push("Datenschutzleiste"); }
                    if (y.length == 0) { y.push("Kein(e)"); }
                    x += addHtmlValue("Benutzereinwilligung", y.join(', '));
                    var type = ''; if (message.p <= 7) { type = ['', "Remote Terminal Link", "Remotedesktop-Link", "Remotedesktop + Terminal-Link", "Link zu Remote-Dateien", "Remote-Terminal + Datei-Link", "Remotedesktop + Datei-Link", "Remotedesktop + Terminal + Dateilink"][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="' + "Link in Zwischenablage kopieren" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></i></div></div>';
                    setModalContent('xxAddAgent', "Gerät freigeben", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getmqttlogin': {
                    if ((currentNode == null) || (currentNode._id != message.nodeid) || (xxdialogMode != null)) return;
                    var x = "Diese Einstellungen können verwendet werden, um MQTT für dieses Gerät zu verbinden." + '<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-Zugangsdaten", 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', "Plugin-Aktion", format('Select the version to downgrade the plugin: {0}', message.info.name) + '<hr />' + vSelect + '<hr />' + "Bitte beachten Sie, dass ein Downgrade nicht empfohlen wird. Bitte tun Sie dies nur für den Fall, dass ein kürzlich durchgeführtes Upgrade einen Defekt aufweist." + + '<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>' + "Navigieren Sie auf die unten stehende URL, gewähren Sie Zugriff und kopieren Sie den Token-Code zurück." + '</div><br />';
                            x += addHtmlValue("Aktivierung", '<a href=\"' + message.url + '\" rel="noreferrer noopener" target="_blank">Browse to this URL</a>');
                            x += addHtmlFormFloating("Code", '<input id=gdcode onkeyup=server_setupGoogleDriveBackupCheck3() class=form-control></input>');
                            setModalContent('xxAddAgent', "Google Drive Backup", x);
                            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => server_setupGoogleDriveBackupEx(3, 'gd2'));
                            Q('gdcode').focus();
                            QE('idx_dlgOkButton', false);
                        }
                    }
                    break;
                }
                case 'pluginError': {
                    setModalContent('xxAddAgent', "Plugin-Fehler", 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 = "Gültige Anmeldung"; c = 'BBD1BB'; xx = ''; if (message.events[i].tn != null) { m = format("Token: {0}", message.events[i].tn); c = '88D188' } }
                            else if (m == 108) { m = "Ungültige 2FA"; c = 'DD9DC3'; xx = 'x'; }
                            else if (m == 109) { m = "Gesperrtes Konto"; c = 'E1BBBB'; xx = 'x'; }
                            else if (m == 110) { m = "Ungültiges Passwort"; 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', "Bisherige Logins", x);
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    break;
                }
                case 'getDeviceDetails': {
                    saveAs(new Blob([message.data], { type: 'application/octet-stream' }), "Geräteliste" + '.' + message.type);
                    break;
                }
                case 'createLoginToken': { // A new login token was created
                    if (xxdialogMode) return;
                    var x = "Notieren Sie sich diesen Benutzernamen und das Passwort, das Passwort kann nicht erneut angezeigt werden." + '<br /><br />';
                    x += addHtmlValue("Name", EscapeHtml(message.name))
                    if (message.expire != 0) { x += addHtmlValue("Verfallen", EscapeHtml(printDateTime(new Date(message.expire)))); }
                    x += addHtmlValue("Benutzername", '<i class="fa-regular fa-copy" title="' + "Link in Zwischenablage kopieren" + '" 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("Passwort", '<i class="fa-regular fa-copy" title="' + "Link in Zwischenablage kopieren" + '" 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', "Anmeldetoken erstellt", x);
                    QV('idx_dlgOkButton', false);
                    Q('idx_dlgCancelButton').innerHTML = "Schließen";
                    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' : '') + '>' + "Agent-Typ" + '</label><br />';
            x += '<label><input id=d2c9 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('agver') >= 0) ? ' checked' : '') + '>' + "Agent-Version" + '</label><br />';
            x += '<label><input id=d2c6 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('desc') >= 0) ? ' checked' : '') + '>' + "Gerätebeschreibung" + '</label><br />';
            x += '<label><input id=d2c5 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('os') >= 0) ? ' checked' : '') + '>' + "Betriebssystem" + '</label><br />';
            x += '<label><input id=d2c10 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('tags') >= 0) ? ' checked' : '') + '>' + "Tags" + '</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' : '') + '>' + "Letzter Startvorgang" + '</label><br />';
            x += '<label><input id=d2c16 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('idletime') >= 0)?' checked':'') + '>' + "Leerlaufzeit" + '</label><br />';
            x += '<label><input id=d2c1 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('links') >= 0) ? ' checked' : '') + '>' + "MeshCentral Router-Links" + '</label><br />';
            x += '<label><input id=d2c2 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('user') >= 0) ? ' checked' : '') + '>' + "Eingeloggte Benutzer" + '</label><br />';
            x += '<label><input id=d2c3 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('ip') >= 0) ? ' checked' : '') + '>' + "Agent IP Adresse" + '</label><br />';
            x += '<label><input id=d2c4 type=checkbox class="form-check-input me-2"' + ((deviceViewSettings.devsCols.indexOf('conn') >= 0) ? ' checked' : '') + '>' + "Server Verbindung" + '</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' : '') + '>' + "Zuletzt gesehen" + '</label><br />';
            setModalContent('xxAddAgent', "Aufteilen der Geräteansicht", 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>' + ", nur Intel&reg; AMT" + '</span>'; }
                            if (meshes[node.meshid] && (meshes[node.meshid].mtype == 3)) { if (meshes[node.meshid].relayid) { extra = '<span class=devHeaderx>' + ", Relaisgeräte" + '</span>'; } else { extra = '<span class=devHeaderx>' + ", Lokale Geräte" + '</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>' + "Einzelne Geräte" + '</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>' + "Einzelne Geräte" + '</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>' + "Einzelne Geräte" + '</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">' + "In keiner Gruppe sind Geräte enthalten. Klicken Sie auf \"Gruppen\" eines Geräts, um es einer Gruppe hinzuzufügen." + '</div>';
                    } else {
                        r = '<div style="margin:30px">' + "Keine Geräte, die auf diese Suche passen." + ' <a onclick=clearDeviceSearch()>' + "Suchfilter löschen" + '</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>' + "Keine Intel&reg; AMT-Geräte in dieser Gerätegruppe";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "eins hinzufügen" + '</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>' + "Keine Geräte in dieser Gerätegruppe";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "eins hinzufügen" + '</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>' + "Keine lokalen Geräte in dieser Gerätegruppe";
                                if (((meshrights & 4) != 0) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { r += ', <a href=# style=cursor:pointer onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "eins hinzufügen" + '</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>' + "Keine Geräte in dieser Gerätegruppe";
                            }
                            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>' + "Agent-Typ"; }
                        if (deviceViewSettings.devsCols.indexOf('agver') >= 0) { colums += '<th style=width:100px>' + "Agent-Version"; }
                        if (deviceViewSettings.devsCols.indexOf('os') >= 0) { colums += '<th style=width:160px>' + "Betriebssystem"; }
                        if (deviceViewSettings.devsCols.indexOf('tags') >= 0) { colums += '<th style=width:120px>' + "Tags"; }
                        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>' + "Letzter Startvorgang"; }
                        if (deviceViewSettings.devsCols.indexOf('idletime') >= 0) { colums += '<th style=width:100px title="' + "Alle 5 Minuten aktualisiert" + '">' + "Leerlaufzeit"; }
                        if (deviceViewSettings.devsCols.indexOf('links') >= 0) { colums += '<th style=width:120px>' + "Links"; }
                        if(features3 & 0x00000002){
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:180px>' + "Benutzer"; }
                        } else {
                            if (deviceViewSettings.devsCols.indexOf('user') >= 0) { colums += '<th style=width:120px>' + "Benutzer"; }
                        }
                        if (deviceViewSettings.devsCols.indexOf('ip') >= 0) { colums += '<th style=width:120px>' + "Adresse"; }
                        if (deviceViewSettings.devsCols.indexOf('conn') >= 0) { colums += '<th style=width:100px>' + "Konnektivität"; }
                        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 Status"; }
                        if (deviceViewSettings.devsCols.indexOf('lastseen') >= 0) {
                            colums += '<th style=width:120px>' + "Zuletzt gesehen";
                            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>' + "Keine Geräte mit Tags gefunden." + '</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="' + "Eine neue Gerätegruppe erstellen." + '" style=cursor:pointer>' + "Gerätegruppe hinzufügen" + '</a>&nbsp';
                }
                if ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 128) == 0)) {
                    r += '<a href=# onclick=\'return p10showMeshCmdDialog(0)\' style=cursor:pointer title="' + "MeshCmd herunterladen, ein Befehlszeilenprogramm, das viele Aufgaben ausführt." + '">' + "MeshCmd" + '</a>&nbsp';
                    if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) { r += '<a href=# onclick=\'return p10showMeshRouterDialog()\' style=cursor:pointer title="' + "MeshCentral Router herunterladen, ein TCP-Port-Mapping-Tool." + '">' + "Router" + '</a>&nbsp'; }
                }
                r += '</div><div style=height:60px></div>';

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

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

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

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

                    for (var i in multiDesktop) { multiDesktop[i].xxdelete = true; }
                    for (var i in kvmDivs) {
                        var id = kvmDivs[i], shortid = id.split('/')[2], desk = multiDesktop[id];
                        if (desk != null) {
                            // This device already has a canvas, use it.
                            var width = (view == 5) ? ((desk.m.width * vsize.y) / desk.m.height) : vsize.x;
                            desk.m.CanvasId.setAttribute('style', 'background-color:black;width:' + width + 'px;height:' + vsize.y + 'px');
                            Q('xkvmid_' + shortid).appendChild(desk.m.CanvasId);
                            delete desk.xxdelete;
                            QH('skvmid_' + shortid, ["Getrennt", "Verbinden...", "Einrichtung...", '', ''][((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, ["Getrennt", "Verbinden...", "Einrichtung...", '', ''][((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>' + "Kein(e)" + '</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>' + "Kein(e)" + '</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 = "Gerät ist angeschlossen"; }
                    if (bat.state == 'dc') { statestr = "Gerät ist batteriebetrieben"; }

                    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="' + "Der Switch-Port ist einsatzbereit." + '">' + "Schalten" + '</span>');
                            } else {
                                states.push('<span title="' + "Der IP-KVM-Port ist angeschlossen und einsatzbereit." + '">' + "IP-KVM" + '</span>');
                            }
                        } else {
                            states.push('<span title="' + "Mesh-Agent ist verbunden und bereit." + '">' + "Agent" + '</span>');
                        }
                    }
                    if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA ist verbunden und einsatzbereit." + '">' + "CIRA" + '</span>'); }
                    else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT ist routebar." + '">' + "AMT" + '</span>'); }
                    if ((node.conn & 8) != 0) { states.push('<span title="' + "Mesh-Agent ist erreichbar mittels eines anderen Agents als Vermittler." + '">' + "Weiterleitung" + '</span>'); }
                    if ((node.conn & 16) != 0) { states.push('<span title="' + "MQTT-Verbindung zum Gerät ist aktiv." + '">' + "MQTT" + '</span>'); }
                }
                if (node.mtype == 3) {
                    var mesh = meshes[node.meshid];
                    if (mesh && mesh.relayid) {
                        states.push('<span title="' + "Lokale Netzwerkverbindung durch einen Relay-Agent" + '">' + "Weiterleitung" + '</span>');
                    } else {
                        states.push('<span title="' + "Lokale Netzwerkverbindung" + '">' + "Lokal" + '</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>' + "PROBLEM" + '</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>' + "PROBLEM" + '</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>' + "PROBLEM" + '</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 ist im Client Control Mode aktiviert" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { amtstate += ' <span title="' + "Intel&reg; AMT ist im Admin Control Mode aktiviert" + '">' + "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 += "Verbunden"; } 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 + '\')">' + "Getrennt" + '</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) + '")>' + "Verwerfen" + '</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', "Hilfeanfragen" + ' - ' + 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>' + "Remotedesktop" + '</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="' + "Trennen Sie diese Sitzung" + '" class="fa-fw fa-solid fa-trash text-danger" ></i>'; }
                            x += addHtmlValue2(getUserName(j), ((node.sessions.kvm[j] == 1) ? "1 Sitzung" : nobreak(format("{0} Sitzungen", node.sessions.kvm[j]))) + trash);
                        } else if (j == 'busy') {
                            x += addHtmlValue2("Gerät ist beschäftigt", ((node.sessions.kvm[j] == 1) ? "1 Sitzung" : nobreak(format("{0} Sitzungen", node.sessions.kvm[j]))));
                        }
                    }
                } else if (i == 'multidesk') {
                    x += '<u>' + "Remotedesktop" + '</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="' + "Trennen Sie diese Sitzung" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.multidesk[j] == 1) ? "1 Sitzung" : nobreak(format("{0} Sitzungen", node.sessions.multidesk[j]))) + trash);
                    }
                } else if (i == 'terminal') {
                    x += '<u>' + "Terminal" + '</u>';
                    for (var j in node.sessions.terminal) {
                        var trash = '';
                        if ((j == userinfo._id) || (GetNodeRights(node) == 0xFFFFFFFF)) { trash = ' <i role=button onclick=\'return endDeviceSession("terminal", "' + encodeURIComponentEx(node._id) + '", "' + encodeURIComponentEx(j) + '")\' title="' + "Trennen Sie diese Sitzung" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.terminal[j] == 1) ? "1 Sitzung" : nobreak(format("{0} Sitzungen", node.sessions.terminal[j]))) + trash);
                    }
                } else if (i == 'files') {
                    x += '<u>' + "Dateien" + '</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="' + "Trennen Sie diese Sitzung" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.files[j] == 1) ? "1 Sitzung" : nobreak(format("{0} Sitzungen", node.sessions.files[j]))) + trash);
                    }
                } else if (i == 'tcp') {
                    x += '<u>' + "TCP-Routing" + '</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="' + "Trennen Sie diese Sitzung" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.tcp[j] == 1) ? "1 Sitzung" : nobreak(format("{0} Sitzungen", node.sessions.tcp[j]))) + trash);
                    }
                } else if (i == 'udp') {
                    x += '<u>' + "UDP-Routing" + '</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="' + "Trennen Sie diese Sitzung" + '" class="fa-fw fa-solid fa-trash text-danger"></i>'; }
                        x += addHtmlValue2(getUserName(j), ((node.sessions.udp[j] == 1) ? "1 Sitzung" : nobreak(format("{0} Sitzungen", node.sessions.udp[j]))) + trash);
                    }
                }
            }
            if (x != '') {
                xxdialogTag = 'SESSIONS-' + node._id;
                setModalContent('xxAddAgent', "Sitzungen" + ' - ' + 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', "Agentennachrichten" + ' - ' + 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} Benutzer", 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) + ' (' + "Gesperrt" + ')', (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', "Alle verbinden", format("Möchten Sie wirklich eine Verbindung zu {0} Geräten herstellen?", 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, ["Getrennt", "Verbinden...", "Einrichtung...", '', ''][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', "Einstellungen des entfernten Desktops", 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="' + "Füge einen neuen Intel&reg;-AMT-Rechner hinzu, der sich im lokalen Netzwerk befindet." + '" onclick=\'return addDeviceToMesh("' + mesh._id + '")\'>' + "Lokales Gerät hinzufügen" + '</a>';
                    r += ' <a href=# role="button" title="' + "Füge einen neuen Intel&reg;-AMT-Rechner durch Scannen des lokalen Netzwerks hinzu." + '" onclick=\'return addAmtScanToMesh("' + mesh._id + '")\'>' + "Netzwerk durchsuchen" + '</a>';
                }
                if (mesh.amt && (mesh.amt.type > 0)) { // CCM Deactivate, CCM or ACM activation, Full Automatic
                    r += ' <a href=# role="button" title="' + "Führen Sie die Aktivierung und Konfiguration von Intel&reg; AMT durch." + '" onclick=\'return showAmtSetup("' + mesh._id + '")\'>' + "Konfiguration" + '</a>';
                }
            }
            if ((mesh.mtype == 2) && ((userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.siteadmin & 4096) == 0))) { // Agent device group
                r += ' <a href=# role="button" title="' + "Füge einen neuen Computer durch Installation des Mesh-Agenten zu dieser Gerätegruppe hinzu." + '" onclick=\'return addAgentToMesh("' + mesh._id + '")\'>' + "Agenten hinzufügen" + '</a>';
                if ((features & 2) == 0) { r += ' <a href=# role="button" title="' + "Laden Sie jemanden ein, den Mesh-Agent für diese Gerätegruppe zu installieren." + '" onclick=\'return inviteAgentToMesh("' + mesh._id + '")\'>' + "Einladen" + '</a>'; }
            }
            if (mesh.mtype == 3) { // Local device group
                r += ' <a href=# role="button" title="' + "Fügen Sie ein Gerät hinzu, das sich im lokalen Netzwerk befindet." + '" onclick=\'return addLocalDeviceToMesh("' + mesh._id + '")\'>' + "Gerät hinzufügen" + '</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("Fügen Sie der Gerätegruppe \"{0}\" ein lokales Gerät hinzu.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Gerätename", '<input id=dp1devicename class=form-control maxlength=32 autocomplete=off onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Hostname", '<input id=dp1hostname class=form-control maxlength=32 autocomplete=off placeholder="' + "Gleich dem Gerätenamen" + '" onchange=validateLocalDeviceToMesh() onkeyup=validateLocalDeviceToMesh() />');
            x += addHtmlFormFloating("Modus", '<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', "Lokales Gerät hinzufügen", 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("Füge ein neues Intel&reg;-AMT-Gerät zur Gerätegruppe \"{0}\" hinzu.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Gerätename", '<input id=dp1devicename class="form-control" maxlength=32 autocomplete=off onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Hostname", '<input id=dp1hostname class="form-control" maxlength=32 autocomplete=off placeholder="' + "Gleich dem Gerätenamen" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Benutzername", '<input id=dp1username class="form-control" maxlength=32 autocomplete=off placeholder="' + "Admin" + '" onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Passwort", '<input id=dp1password type=password class="form-control" autocomplete=off maxlength=32 onchange=validateDeviceToMesh() onkeyup=validateDeviceToMesh() />');
            x += addHtmlFormFloating("Sicherheit", '<select id=dp1tls class="form-select"><option value=0>' + "Keine TLS-Sicherheit" + '</option><option value=1>' + "TLS-Sicherheit vorausgesetzt" + '</option></select>');

            setModalContent('xxAddAgent', "Intel&reg;-AMT-Gerät hinzufügen", 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 = "Erstellen Sie mehrere Intel&reg; AMT Geräte gleichzeitig durch Import einer JSON Datei mit dem folgenden 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 Geräteimport", 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 Geräteimport", format("Ungültige JSON-Datei: {0}.", ex));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    return;
                }
                if (j != null) {
                    meshserver.send({ action: 'importamtdevices', meshid: meshid, amtdevices: j });
                } else {
                    setModalContent('xxAddAgent', "Intel&reg; AMT Geräteimport", "Ungültiges JSON-Dateiformat.");
                    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("Fügen Sie Intel&reg; AMT hinzu, aktivieren Sie es und konfigurieren Sie es so, dass es \"{0}\" gruppiert, indem Sie MeshCmd regelmäßig als Administrator auf dem Remote-Gerät ausführen.", 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>' + "Für die ACM-Aktivierung muss Intel&reg; AMT auf den folgenden vertrauenswürdigen vollqualifizierten Domänennamen eingestellt sein:" + ' <b>' + serverinfo.amtAcmFqdn.join(', ') + '</b></div>');
            }
            setModalContent('xxAddAgent', "Intel&reg; AMT-Setup", 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>' + "Aktivieren Sie Intel&reg; AMT im Admin Control Mode (ACM) mit einem FAT-formatierten USB-Stick. Legen Sie setup.bin darauf und starten Sie einen oder mehrere Computer mit diesem Schlüssel." + '</div><div style=margin-top:6px>' + "Geben Sie zunächst das alte und das neue MEBx-Passwort ein." + '</div></table>';
            x += addHtmlFormFloating("Altes Passwort", '<input id=dp1password0 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Neues Passwort*", '<input id=dp1password1 type=password class="form-control" autocomplete=off maxlength=32 onchange=validateAmtAcmSetupEx() onkeyup=validateAmtAcmSetupEx() />');
            x += addHtmlFormFloating("Neues Passwort*", '<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" /> ' + "Für Bare-Metal-LAN-Aktivierung verwenden." + '</label>'; } // Intel AMT LAN provisioning server is active.
            x += '<div><span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 Zeichen, 1 großes, 1 kleines, 1 numerisches, 1 nicht alphanumerisches Zeichen." + '</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 = "Geben Sie einen Bereich von IP-Adressen ein, um nach Intel&reg; AMT-Geräten zu suchen." + '<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-Bereich", '<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="' + "Scannen" + '" onclick=addAmtScanToMeshButton()></input></tr></table>');
            } else {
                x += addHtmlValue("IP-Bereich", '<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="' + "Scannen" + '" 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="' + "Abbrechen" + '" style="" onclick="dialogclose(0)" /><input id="idx_dlgOkButton2" type="button" value="OK" style="" onclick="dialogclose(1)" /><div><input id="d2scanSelectButton" type="button" value="' + "Alle auswählen" + '" onclick="scanSelectAll()" /></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Nach Intel&reg;-AMT-Geräten suchen", 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>' + "Beispiel für IP-Bereichswerte" + '<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>' + "Scanne..." + '</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("Einladungstyp", '<select id=d2InviteType onchange=d2ChangedInviteType() class=form-select><option value=0>' + "Link-Einladung" + '</option><option value=1>' + "E-Mail-Einladung" + '</option></select>') + '<hr />';
                x += '<div id=emailInviteDiv style=display:none>' + format("Laden Sie jemanden ein, den Mesh-Agent zu installieren. Eine E-Mail mit einem Link zur Mesh-Agent-Installation für die Gerätegruppe \"{0}\" wird verschickt.", EscapeHtml(mesh.name)) + '<br /><br />';
                x += addHtmlFormFloating("Name (optional)", '<input id=agentInviteName value="" placeholder="Name (optional)" class="form-control" maxlength=64 />');
                x += addHtmlFormFloating("E-Mail", '<input id=agentInviteEmail placeholder="' + "example@email.com" + '" class="form-control" onkeyup=validateAgentInvite() />');
                x += addHtmlFormFloating("Betriebssystem", '<select id=agentInviteNameOs onchange=d2ChangedInviteType() class=form-select><option value=4>' + "Sende Installations-Link" + '</option><option value=0 selected>' + "Alle unterstützten" + '</option><option value=1>' + "nur Windows" + '</option><option value=3>' + "Nur Apple macOS" + '</option><option value=2>' + "nur Linux" + '</option><option value=5>' + "MeshCentral-Assistent" + '</option></select>');
                x += '<div id=d2agentexpirediv>';
                x += addHtmlFormFloating("Link-Ablauf", '<select id=agentInviteExpire class=form-select><option value=1>' + "1 Stunde" + '</option><option value=8>' + "8 Stunden" + '</option><option value=24>' + "1 Tag" + '</option><option value=168>' + "1 Woche" + '</option><option value=5040>' + "1 Monat" + '</option><option value=0>' + "Unbegrenzt" + '</option></select>');
                x += '</div>';
                x += '<div id=d2agentInstallTypeDiv2>';
                x += addHtmlFormFloating("Installationstyp", '<select id=agentInviteType class=form-select><option value=0>' + "Hintergrund und interaktiv" + '</option><option value=2>' + "Nur Hintergrund" + '</option><option value=1>' + "Nur interaktiv" + '</option></select>');
                x += '</div>';
                x += addHtmlFormFloating("Nachricht" + ' ' + "(optional)", '<textarea id="agentInviteMessage" class="form-control" style="height:100px;resize:none;" maxlength="1024"></textarea>');
                x += '</div>';
            }
            x += '<div id=urlInviteDiv>' + format("Laden Sie jemanden ein, den Mesh-Agent zu installieren, indem Sie einen Einladungslink freigeben. Dieser Link verweist den Benutzer auf Installationsanweisungen für die Gerätegruppe \"{0}\". Der Link ist öffentlich und es wird kein Konto für diesen Server benötigt.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlFormFloating("Link-Ablauf", '<select id=d2inviteExpire class=form-select onchange=d2RequestInvitationLink()><option value=1>' + "1 Stunde" + '</option><option value=8>' + "8 Stunden" + '</option><option value=24>' + "1 Tag" + '</option><option value=168>' + "1 Woche" + '</option><option value=5040>' + "1 Monat" + '</option><option value=0>' + "Unbegrenzt" + '</option></select>');
            x += addHtmlFormFloating("Agent", '<select id=d2agentType class=form-select onchange=d2ChangedInviteType()><option value=0>' + "Alle verfügbaren Agents" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "MeshCentral-Assistent" + '</option><option value=16>' + "Android MeshAgent" + '</option></select>');
            x += '<div id=d2agentInstallTypeDiv>';
            x += addHtmlFormFloating("Installationstyp", '<select id=d2agentInviteType class=form-select onchange=d2RequestInvitationLink()><option value=0>' + "Hintergrund und interaktiv" + '</option><option value=2>' + "Nur Hintergrund" + '</option><option value=1>' + "Nur interaktiv" + '</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="' + "Link in Zwischenablage kopieren" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></div></div>';
            xxdialogTag = meshid;
            setModalContent('xxAddAgent', "Einladen", 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>' + "Binäres Linux / BSD / macOS-Installationsprogramm" + '</option><option value=2>' + "Apple macOS" + '</option>';
            if ((features & 2) == 0) { opts += '<option value=6>' + "Mobiles Gerät" + '</option>'; } // Don't display mobile setup in LAN mode.
            opts += '<option value=7>' + "MeshCentral-Assistent" + '</option>';
            opts += '<option value=3>' + "Windows (Deinstallation)" + '</option><option value=4>' + "Linux / BSD (Deinstallation)" + '</option><option value=8>' + "Apple macOS (Uninstall)" + '</option></select>';
            x += addHtmlFormFloating("Betriebssystem", 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("Systemtyp", '<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("Installationstyp", '<select id="aginsType" onchange="addAgentToMeshClick()" class="form-select"><option value="0">' + "Hintergrund & interaktiv" + '</option><option value="2">' + "Nur Hintergrund" + '</option><option value="1">' + "Nur interaktiv" + '</option></select>');
            x += '</div><div id="asinsTypeDiv">';
            x += addHtmlFormFloating("Installationstyp", '<select id="asinsType" onchange="addAgentToMeshClick()" class="form-select"><option value="2">' + "Anwendung, nach Benutzeranforderung verbinden" + '</option><option value="3">' + "Anwendung, immer verbunden" + '</option><option value="0">' + "Taskleiste, Verbinden auf Benutzerwunsch" + '</option><option value="1">' + "Taskleiste, Immer verbunden" + '</option><option value="4">' + "Taskleiste, nur Monitor" + '</option></select>');
            x += '</div><hr>';

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

            // Windows agent install
            x += '<div id=agins_windows>' + format("Um einen neuen Rechner zur Gerätegruppe \"{0}\" hinzuzufügen, laden Sie den Mesh-Agenten herunter und installieren Sie ihn auf dem zu verwaltenden Rechner. In diesen Agenten sind Server- und Gerätegruppen-Informationen bereits eingebettet.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline id=aginsw32lnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 32bit Version des MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Windows x86 32bit Agent-URL in die Zwischenablage kopieren" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline id=aginsw64lnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 64bit Version des MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Windows x86 64bit Agent-URL in die Zwischenablage kopieren" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline id=aginswa64lnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "ARM 64bit Version des MeshAgents" + '">' + "Windows ARM (64bit) .exe" + '</a> <i class="fa-regular fa-clipboard" title="' + "Windows ARM 64bit Agent-URL in die Zwischenablage kopieren" + '" 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("Einstellungsdatei", '<a id=aginswmshlnk name="meshsettings?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + format("{0} Einstellungen (.msh)", EscapeHtml(mesh.name)) + '</a>'); }
            x += '</div>';

            // Linux agent install
            x += '<div id=agins_linux style=display:none>' + format("Um einen Rechner zu \"{0}\" hinzuzufügen, führe den folgenden Befehl aus. Root-Zugriff wird benötigt.", 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>' + "* Unter BSD, führe zunächst \"pkg install wget sudo bash\" aus." + '</div><a style=text-decoration:none title="' + "In Zwischenablage kopieren" + '" onclick=copyAgentIdValue("agins_linux_area")>' + "Kopie" + ' <i class="fa-regular fa-clipboard" style=cursor:pointer></i></a></div>';

            // macOS agent install
            x += '<div id=agins_osx style=display:none>' + format("Um einen neuen Rechner zur Gerätegruppe \"{0}\" hinzuzufügen, laden Sie den Mesh-Agenten herunter und installieren Sie ihn auf dem zu verwaltenden Rechner. In dieses Agenten-Installationsprogramm sind Server- und Gerätegruppen-Informationen bereits eingebettet.", EscapeHtml(mesh.name)) + '<br /><br />';
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=16&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "x86-64bit version of macOS Mesh Agent" + '">' + "macOS x86-64bit" + '</a> <i class="fa-regular fa-clipboard" title="' + "macOS-Agenten-URL in Zwischenablage kopieren" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=16&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=29&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "ARM 64bit Version von macOS Mesh Agent" + '">' + "macOS ARM (64bit)" + '</a> <i class="fa-regular fa-clipboard" title="' + "macOS-Agenten-URL in Zwischenablage kopieren" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=29&meshid=' + meshid.split('/')[2] + '",0)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Universal version of macOS Mesh Agent" + '">' + "macOS (Universal)" + '</a> <i class="fa-regular fa-clipboard" title="' + "macOS-Agenten-URL in Zwischenablage kopieren" + '" 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("Laden Sie die MeshAgent-Anwendung herunter und scannen Sie diesen QR-Code, um der Gruppe \"{0}\" ein mobiles Gerät hinzuzufügen.", EscapeHtml(mesh.name)) + '</div>';
            x += '<table style=width:180px>';
            x += '<tr><td style=text-align:center><a title="' + "Google Play Store" + '"rel="noreferrer noopener" target=_blank href="https://play.google.com/store/apps/details?id=com.meshcentral.agent2"><img style=cursor:pointer src="images/google-play-140.png" width=140 srcset="images/google-play-280.png 2x" /></a></td></tr>';
            x += '<tr><td style=text-align:center><a title="' + "Amazon App Store" + '" rel="noreferrer noopener" target=_blank href="https://www.amazon.co.uk/gp/product/B097Z4Q7SK/"><img style=cursor:pointer src="images/amazon-appstore-140.png"  width=140 srcset="images/amazon-appstore-280.png 2x" /></a></td></tr>';
            x += '</table>';
            x += addHtmlValue("Android-APK", '<a class=text-decoration-underline onclick=downloadFile("meshagents?id=14' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '",null,true) title="' + "APK Version von MeshAgent" + '">' + "APK" + '</a> <i class="fa-regular fa-clipboard" title="' + "URL in Zwischenablage kopieren" + '" 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 ist ein Windows-Tool, mit dem Benutzer um Hilfe bitten können. Verwenden Sie den unten stehenden Link, um eine Version herunterzuladen, die eine Verbindung zur Gerätegruppe \"{0}\" herstellt.", 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 für Windows" + '">' + "Assistent für Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "URL in Zwischenablage kopieren" + '" 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 ist ein Windows-Tool, mit dem Benutzer um Hilfe bitten können. Verwenden Sie den unten stehenden Link, um eine Version herunterzuladen, die den Hintergrundagenten überwacht." + '</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 für Windows" + '">' + "Assistent für Windows (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "URL in Zwischenablage kopieren" + '" 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>' + "Um den Mesh-Agenten zu entfernen, laden Sie die untenstehende Datei herunter, führen Sie sie aus und klicken Sie auf \"Deinstallieren\"." + '<br /><br />';
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline id=aginsw32unlnk name="meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 32bit Version des MeshAgent" + '">' + "Windows x86-32 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Windows x86 32bit Agent-URL in die Zwischenablage kopieren" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=3&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline id=aginsw64unlnk name="meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "x86 64bit Version des MeshAgent" + '">' + "Windows x86-64 (.exe)" + '</a> <i class="fa-regular fa-clipboard" title="' + "Windows x86 64bit Agent-URL in die Zwischenablage kopieren" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=4&meshid=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline id=aginswa64unlnk name="meshagents?id=43&meshid=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '" title="' + "ARM 64bit Version des MeshAgents" + '">' + "Windows ARM (64bit) .exe" + '</a> <i class="fa-regular fa-clipboard" title="' + "Windows ARM 64bit Agent-URL in die Zwischenablage kopieren" + '" 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>' + "Um einen Mesh-Agent zu entfernen, führen Sie den folgenden Befehl aus. Root-Zugriff wird benötigt." + '<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="' + "In Zwischenablage kopieren" + '" 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>' + "Um den Mesh-Agenten zu entfernen, laden Sie die untenstehende Datei herunter, entpacken Sie das ZIP-Archiv, klicken Sie dann mit der rechten Maustaste auf \"Uninstall.command\" und wählen Sie \"Öffnen\"." + '<br /><br />';
            x += addHtmlValue("Mesh-Agent", '<a class=text-decoration-underline onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") title="' + "Universal version of macOS Mesh Agent" + '">macOS Agent (Universal)</a> <i class="fa-regular fa-clipboard" title="' + "macOS-Agenten-URL in Zwischenablage kopieren" + '" 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-Agent", '<a class=text-decoration-underline id=aginsbinlnk name="meshagents?id=' + meshid.split('/')[2] + '&installflags=0' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '">' + "Mesh-Agent" + '</a> <i class="fa-regular fa-clipboard" title="' + "Kopieren Sie die Agenten-URL in die Zwischenablage" + '" style=cursor:pointer onclick=copyAgentUrl("meshagents?id=' + meshid.split('/')[2] + '&installflags=",1)></i>', ['col-md-4', 'col-md-8']);
            x += addHtmlValue("Befehl" + '&nbsp;<i class="fa-regular fa-clipboard" title="' + "Kopieren Sie die Agenten-URL in die Zwischenablage" + '" style=cursor:pointer onclick=copyAgentIdValue("aginsbincmd")></i>', '<input id=aginsbincmd type=text class="form-control" readonly value=\'wget -O meshagent' + (((features & 0x80000000) != 0) ? ' --no-check-certificate' : '') + ' \"https://' + servername + portStr + domainUrl + 'meshagents?id=' + meshid.split('/')[2].split('$').join('%24').split('@').join('%40') + '\' />', ['col-md-4 d-flex align-items-center', 'col-md-8']);
            x += '</div>';

            setModalContent('xxAddAgent', "Mesh-Agenten hinzufügen", 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 Knoten" : format("{0} Knoten", 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="' + "Gerät ist eingeschaltet." + '">' + "eingeschaltet" + '</span>', '<span title="' + "Gerät befindet sich im Stand-By (S1)." + '">' + "Ruhezustand" + '</span>', '<span title="' + "Gerät befindet sich im Ruhezustand (S2)." + '">' + "Ruhezustand" + '</span>', '<span title="' + "Gerät befindet sich im Tiefschlaf (S3)." + '">' + "Tiefschlaf" + '</span>', '<span title="' + "Gerät befindet sich im Ruhezustand (S4)." + '">' + "Ruhezustand" + '</span>', '<span title="' + "Gerät ist ausgeschaltet (S5)." + '">' + "Soft-Off" + '</span>', '<span title="' + "Gerät wurde erkannt, aber der Energiezustand konnte nicht ermittelt werden." + '">' + "präsent" + '</span>', '<span title="' + "Gerät ist ausgeschaltet." + '">' + "aus" + '</span>'];
        var powerStateStrings2 = ['', "Gerät ist eingeschaltet", "Gerät befindet sich im Stand-By (S1)", "Gerät befindet sich im Ruhezustand (S2)", "Gerät befindet sich im Tiefschlaf (S3)", "Gerät ist im Ruhezustand (S4)", "Gerät ist im Soft-Off-Zustand (S5)", "Gerät ist präsent, aber der Energiezustand kann nicht ermittelt werden", "Das Gerät ist ausgeschaltet"];
        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="' + "Netzschalter ist betriebsbereit." + '">' + "Schalten" + '</span>');
                        } else {
                            states.push('<span title="' + "Der IP-KVM-Port ist aktiviert und einsatzbereit." + '">' + "IP-KVM" + '</span>');
                        }
                    } else {
                        states.push('<span title="' + "Mesh-Agent ist verbunden und bereit." + '">' + "Agent" + '</span>');
                    }
                }
                if ((node.conn & 2) != 0) { states.push('<span title="' + "Intel&reg; AMT CIRA ist verbunden und einsatzbereit." + '">' + "CIRA" + '</span>'); }
                else if ((node.conn & 4) != 0) { states.push('<span title="' + "Intel&reg; AMT ist routebar." + '">' + "AMT" + '</span>'); }
                if ((node.conn & 8) != 0) { states.push('<span title="' + "Mesh-Agent ist erreichbar mittels eines anderen Agents als Vermittler." + '">' + "Weiterleitung" + '</span>'); }
                if ((node.conn & 16) != 0) { states.push('<span title="' + "MQTT-Verbindung zum Gerät ist aktiv." + '">' + "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 "Unbekannt";
        }

        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 = "Auswahl aufheben";
                QV('cxmdesktop', true);
            } else {
                QE('GroupActionButton', false);
                Q('SelectAllButton').value = "Alle auswählen";
                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', "Konto-Sicherheit", "Der Zugriff auf diese Funktion ist erst möglich, wenn die Zwei-Faktor-Authentifizierung aktiviert ist. Dies ist für zusätzliche Sicherheit erforderlich. Gehen Sie zur Registerkarte \"Mein Konto\" und sehen Sie sich den Abschnitt \"Kontosicherheit\" an.");
                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: "Sende MQTT-Nachricht" });; break; } }
            }

            // Display the options that are allowed depending on what devices are selected.
            addedOptions.push({ v: 105, name: "Geräteinformationen exportieren", 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: "In Gerätegruppe verschieben" }); }
                if (((node.conn & 1) != 0) && ((rights & 0x8000) != 0) && ((added & 1) == 0)) { added |= 1; addedOptions.push({ v: 104, name: "Agent deinstallieren" }); }
                if ((rights & 64) && ((added & 8) == 0)) { added |= 8; addedOptions.push({ v: 100, name: "Geräte aufwecken" }); }
                if ((rights & 262144) && ((added & 4) == 0)) { added |= 4; addedOptions.push({ v: 4, name: "Geräte in Ruhezustand versetzen" }); addedOptions.push({ v: 3, name: "Geräte zurücksetzen" }); addedOptions.push({ v: 2, name: "Geräte ausschalten" }); }
                if ((rights & 131072) && ((added & 16) == 0)) { added |= 16; addedOptions.push({ v: 106, name: "Befehle ausführen" }); }
                if ((rights & 16384) && ((added & 32) == 0)) { added |= 32; addedOptions.push({ v: 108, name: "Gerätebenachrichtigung" }); }
                if ((rights & 4) && ((added & 64) == 0)) { added |= 64; addedOptions.push({ v: 107, name: "Stichworte bearbeiten" }); }
                if ((rights & 8) && ((added & 256) == 0)) { added |= 256; addedOptions.push({ v: 109, name: "Daten hochladen" }); }
                if ((rights & 32768) && ((added & 128) == 0)) { added |= 128; addedOptions.push({ v: 101, name: "Geräte löschen" }); }
                if ((node.agent != null) && (features2 & 0x00000010) && (rights == 0xFFFFFFFF) && ((added & 512) == 0)) {
                    added |= 512;
                    addedOptions.push({ v: 110, name: "Aktualisierung des Agents erzwingen" });
                    addedOptions.push({ v: 111, name: "Entferne Agent." });
                    addedOptions.push({ v: 112, name: "Vorgabe-Serverkern hochladen" });
                }
            }

            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 = "Wählen Sie eine Operation für die Durchführung auf allen ausgewählten Geräten. Aktionen werden nur bei entsprechenden Rechten durchgeführt." + '<br /><br />';
            x += addHtmlFormFloating("Vorgang", '<select id=d2groupop class="form-select">' + addedOptionsStr + '</select>');

            setModalContent('xxAddAgent', "Gruppenaktion", 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 += "Entfernen des ausgewählten Geräts bestätigen?" + '<br /><br />'; }
                else {
                    x += format("Entfernen von {0} ausgewählten Geräten bestätigen?" + '<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 ausgewähltes Gerät ist online." + '<br />', nconn); }
                    else if (nconn > 1) { y += format("{0} ausgewählte Geräte sind online." + '<br />', nconn); }
                    if (ndisc == 1) { y += format("1 ausgewähltes Gerät ist offline." + '<br />', ndisc); }
                    else if (ndisc > 1) { y += format("{0} ausgewählte Geräte sind offline." + '<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() />' + "Bestätigen" + '</label>';
                setModalContent('xxAddAgent', "Geräte löschen", 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: "Führen Sie Befehle auf ausgewählten Geräten aus.", func: uncheckAllDevices });
            } else if (op == 107) {
                // Edit tags
                var x = "Führen Sie einen Batch-Geräte-Tag-Vorgang durch" + '<br /><br />';
                x += addHtmlFormFloating("Vorgang", '<select id=d2deviceop class="form-select"><option value=1>' + "Tags hinzufügen" + '</option><option value=2>' + "Tags setzen" + '</option><option value=3>' + "Tags entfernen" + '</option></select>');
                x += addHtmlFormFloating("Tags", '<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', "Geräte-Tags bearbeiten", 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>'+ "Führen Sie eine Batch-Gerätebenachrichtigung durch" + '</div>';
                x += '<select id=d2deviceop class="form-select-sm me-2" style=width:100%;margin-bottom:4px><option value=2>' + "Toastbenachrichtigung" + '</option><option value=1>' + "Nachrichtenbox" + '</option><option value=3>' + "Alert Box" + '</option></select>';
                x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Titel" + '" style=width:100%;box-sizing:border-box;margin-bottom:4px />';
                x += '<textarea id=d2notifyMsg style=width:100%;height:140px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px></textarea>';
                x += '<select style=width:100% id=d2notifyTimeout>';
                x += '<option disabled value="">Only Applicable to Message Box Notifications</option>';
                x += '<option value=2 selected>' + "2 Minuten lang anzeigen (Standard)" + '</option>';
                x += '<option value=10>' + "10 Minuten lang anzeigen" + '</option>';
                x += '<option value=30>' + "30 Minuten lang anzeigen" + '</option>';
                x += '<option value=60>' + "60 Minuten lang anzeigen" + '</option>';
                x += '<option value=0>' + "Nachricht anzeigen, bis sie vom Benutzer geschlossen wird" + '</option>';
                x += '</select>';
                setModalContent('xxAddAgent', "Gerätebenachrichtigung", 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 = "Laden Sie ausgewählte Dateien auf alle ausgewählten Geräte hoch" + '<br /><br />';
                x += '<form method=post enctype=multipart/form-data action=uploadfilebatch.ashx target=fileUploadFrame>';
                x += '<input type=hidden name=authCookie value=' + authCookie + ' /><input type=hidden name=nodeIds value=' + chkNodeIds.join(',') + ' /><input type=submit id=d2batchUploadSubmit style=display:none />';
                x += '<input type=file name=files id=d2uploadinput class="form-control" multiple=multiple onchange="d2batchUploadValidate()" /><br /><br />';
                if (wintype) { x += addHtmlFormFloating("Windows-Pfad", '<input type=text class="form-control" onchange="d2batchUploadValidate()" onkeyup="d2batchUploadValidate()" name=winpath id=d2winuploadpath placeholder="C:\\temp" value="" />'); }
                if (linuxtype) { x += addHtmlFormFloating("Linux-Pfad", '<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 />' + "Ordner erstellen, falls nicht vorhanden?" + '</label><br />';
                x += '<label><input type=checkbox class="form-check-input me-2" name=overwriteFiles />' + "Überschreiben, wenn Datei vorhanden ist?" + '</label></form>';
                setModalContent('xxAddAgent', "Batch-Datei hochladen", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2batchUploadValidateOk());
                d2batchUploadValidate();
            } else if (op == 110) {
                // Force agent update
                var x = "Agentaktualisierung auf ausgewählten Geräten erzwingen?" + '<br /><br />';
                setModalContent('xxAddAgent', "Aktualisierung des Agents erzwingen", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentUpdateExec());
            } else if (op == 111) {
                // Clear agent core
                var x = "Agent auf ausgewählten Geräten löschen?" + '<br /><br />';
                setModalContent('xxAddAgent', "Entferne Agent.", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => d2groupActionFunctionAgentClearCoreExec());
            } else if (op == 112) {
                // Upload default server core
                var x = "Standard-Server auf ausgewählten Geräten hochladen?" + '<br /><br />';
                setModalContent('xxAddAgent', "Vorgabe-Serverkern hochladen", 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 = "Laden Sie die Liste der Geräte mit einem der folgenden Dateiformate herunter." + '<br /><br />';
            x += addHtmlValue("CSV-Format", '<a href=# style=cursor:pointer onclick=\'return p2downloadDeviceInfoCSV()\'>' + 'devicelist.csv' + '</a>');
            x += addHtmlValue("JSON-Format", '<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 />' + "Gerätedetails einschließen" + '</label></div>';
            setModalContent('xxAddAgent', "Geräteinformationsexport", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

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

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

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

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

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

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

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

                var x = '';
                if (options.title) { x += options.title + '<br />'; }
                x += '<select id=d2cmdtype onclick=d2runCommandValidate() style=width:100%;margin-bottom:4px;margin-top:4px class=form-select>';
                if (wintype == true) { x += '<option value=1' + ((runopt.type == 1) ? ' selected' : '') + '>' + "Windows-Eingabeaufforderung" + '</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-Befehlsshell" + '</option>'; }
                if (agenttype == true) { x += '<option value=4' + ((runopt.type == 4) ? ' selected' : '') + '>' + "Agent-Konsole" + '</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' : '') + '>' + "Als Agent ausführen" + '</option><option value=1' + ((runopt.runAs == 1) ? ' selected' : '') + '>' + "Als Benutzer ausführen, Agent, wenn kein Benutzer vorhanden ist" + '</option><option value=2' + ((runopt.runAs == 2) ? ' selected' : '') + '>' + "Muss als Benutzer ausgeführt werden" + '</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' : '') + '>' + "Befehle aus Textfeld" + '</option><option value=1' + ((runopt.source == 1) ? ' selected' : '') + '>' + "Befehle aus Datei" + '</option>';
                    if (userinfo.siteadmin & 8) { x += '<option value=2' + ((runopt.source == 2) ? ' selected' : '') + '>' + "Befehle aus Datei vom 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', "Befehle ausführen", 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(' ' + "und" + ' '), 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(' ' + "oder" + ' '), 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-Remoteverbindungsport:" + '<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-Verbindung", 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-Remote-Verbindungsport:" + '<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-Verbindung", 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-Remote-Verbindungsport:" + '<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-Verbindung", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                // Save the new RFB port to the server
                var rfbport = ((Q('d10rfbport').value.length > 0) ? parseInt(Q('d10rfbport').value) : 3389);
                meshserver.send({ action: 'changedevice', nodeid: currentNode._id, rfbport: rfbport });
                //if (currentNode != null) { p10rfb(currentNode._id, rfbport); }
            });
            Q('d10rfbport').focus();
            if (currentNode.rfbport != null) { Q('d10rfbport').value = currentNode.rfbport; }
        }

        function cmhttpportaction(action) {
            if (xxdialogMode) return;
            var x = "Port für HTTP-Fernverbindung:" + '<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 Verbindung", 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-Fernverbindungsport:" + '<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 Verbindung", 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', "Umbenennen", '<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("Datei-Editor", "Nur Dateien kleiner als 200 kB können bearbeitet werden."); }
            } else if (action == 3) {
                // Delete the file
                setModalContent('xxAddAgent', "Löschen", "Element löschen?");
                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: "Knotenstandort ändern", callback: function (obj) { modifyMarkerloc(obj.data); } };

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

            // Save Marker item
            map_cm_saveMarker = { text: "Knoten-Standort speichern", callback: function (obj) { saveMarkerloc(obj.data); } };

            // Build a context menu for a feature
            map_cm_nodemenu_items = [
                { text: "Allgemeine Information", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 10); } } },
                { text: "Desktop", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 11); } } },
                { text: "Terminal", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 12); } } },
                { text: "Intel&reg; AMT", callback: function (obj) { if (obj.data != null) { gotoDevice(obj.data, 14); } } },
                '-',
                { text: "Bis auf Fläche hineinzoomen", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 19); } },
                { text: "Bis auf Fläche herauszoomen", callback: function (obj) { var coords = obj.data.getGeometry().getCoordinates(); zoomToLocation(coords, 2); } }
            ];

            // Context menu for clicks other than on feature
            contextmenu_items = [
                { text: "Aktualisieren", callback: function () { refreshMap(true, true); } },
                { text: "Zoomen um Fläche zu entsprechen", callback: function () { zoomToFitExtent(); } },
                { text: "Karte hier zentrieren", callback: function (obj) { xxmap.mapView.animate({ center: obj.coordinate }); } },
                { text: "Knoten hier ablegen", 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 == "Bis auf Fläche hineinzoomen" || item.text == "Bis auf Fläche herauszoomen") { 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>' + "Suche" + '</label>&nbsp&nbsp<input type=text placeholder="' + "Gerätename" + '" 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>' + "Keine Geräte gefunden." + '</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', "Wähle einen Knoten zum Ablegen", 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>' + "Kein Standort gefunden." + '<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', "Konto-Sicherheit", "Der Zugriff auf diese Funktion ist erst möglich, wenn eine E-Mail-Adresse bestätigt wurde. Dies ist für die Kennwortwiederherstellung erforderlich. Gehen Sie zur Registerkarte \"Mein Konto\", um eine E-Mail-Adresse zu ändern und zu überprüfen.");
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
                return;
            }

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && (count2factoraAuths() == 0)) {
                setModalContent('xxAddAgent', "Konto-Sicherheit", "Der Zugriff auf diese Funktion ist erst möglich, wenn die Zwei-Faktor-Authentifizierung aktiviert ist. Dies ist für zusätzliche Sicherheit erforderlich. Gehen Sie zur Registerkarte \"Mein Konto\" und sehen Sie sich den Abschnitt \"Kontosicherheit\" an.");
                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 = "Gerät ist angeschlossen"; }
                    if (bat.state == 'dc') { statestr = "Gerät ist batteriebetrieben"; }

                    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>' + "Kein(e)" + '</i>'; }
                if (((meshrights & 4) != 0) && ((!mesh.flags) || ((mesh.flags & 2) == 0 || (mesh.flags & 16)))) { nname = '<span tabindex=0 title="' + "Hier klicken, um den serverseitigen Gerätenamen zu bearbeiten" + '" 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("Name", nnameEx); }

                // Attribute: Mesh
                if (mesh) { x += addDeviceAttribute('<span title="' + "Der Name der Gerätegruppe zu der dieser Rechner gehört." + '">' + "Gruppe" + '</span>', '<a href=# title="' + "Der Name der Gerätegruppe zu der dieser Rechner gehört" + '" 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="' + "Der im Betriebssystem festgelegte Name dieses Computers" + '">' + "Name im Betriebssystem" + '</span>', '<span title="' + "Der im Betriebssystem festgelegte Name dieses Computers" + '">' + 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("Hostname", addLinkConditional((node.host ? EscapeHtml(node.host) : ('<i>' + "Kein(e)" + '</i>')), 'showEditNodeValueDialog(1)', meshrights & 4));
                }

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

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

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

                // Attribute: Intel AMT
                if (node.intelamt != null) {
                    var str = '';
                    var provisioningStates = { 0: nobreak("Nicht aktiviert (Pre)"), 1: nobreak("Nicht aktiviert (In)"), 2: nobreak("Aktiviert") };
                    if (node.intelamt.ver != null && node.intelamt.state == null) { str += '<i>' + "Unbekannter Zustand" + '</i>, v' + EscapeHtml(node.intelamt.ver); }
                    else if ((node.intelamt.ver == null) && (node.intelamt.state == 2)) { str += '<i>' + "Aktiviert" + '</i>'; }
                    else if ((node.intelamt.ver == null) || (node.intelamt.state == null)) { str += '<i>' + "Unbekannte Version & Zustand" + '</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 ist im Client Control Mode aktiviert" + '">' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { str += ' <span title="' + "Intel&reg; AMT ist im Admin Control Mode aktiviert" + '">' + "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 ist mit TLS-Netzwerksicherheit eingerichtet" + '">' + "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="' + "Zugangsdaten für Intel&reg; AMT bearbeiten" + '" onclick=editDeviceAmtSettings("' + node._id + '")>' + "Keine Zugangsdaten" + '</i>';
                                editUserCredentialsIcon = true;
                            } else {
                                str += ', <i style=color:#FF0000>' + "Keine Zugangsdaten" + '</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 = "Ungültige Anmeldeinformationen"; }
                            if ((node.intelamt.warn & 8) != 0) { warn = "Anmeldeinformationen versuchen"; }
                            if (warn != null) {
                                if ((meshrights & 4) != 0) {
                                    str += ', <i style=color:#FF0000;cursor:pointer title="' + "Zugangsdaten für Intel&reg; AMT bearbeiten" + '" 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="' + "Zugangsdaten für Intel&reg; AMT bearbeiten" + '" style=cursor:pointer onclick=editDeviceAmtSettings("' + node._id + '")>';
                        }
                    }

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

                if ((node.agent != null) && (node.agent.tag != null)) {
                    // Attribute: Mesh Agent Tag
                    var tag = EscapeHtml(node.agent.tag);
                    if (tag.startsWith('mailto:')) { tag = '<a href="' + EscapeHtml(tag) + '">' + EscapeHtml(tag.substring(7)) + '</a>'; }
                    x += addDeviceAttribute("Agent-Markierung", 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 Kennzeichen", 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("Betriebssystem", EscapeHtml(node.osdesc)); }

                // Windows Security Central
                if (node.wsc) {
                    var y = [];
                    if (node.wsc.antiVirus != null) { if (node.wsc.antiVirus == 'OK') { y.push("Antivirus" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Antivirus" + ' - <span style=color:red>' + "PROBLEM" + '</span>'); } }
                    if (node.wsc.autoUpdate != null) { if (node.wsc.autoUpdate == 'OK') { y.push("Aktualisierung" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Aktualisierung" + ' - <span style=color:red>' + "PROBLEM" + '</span>'); } }
                    if (node.wsc.firewall != null) { if (node.wsc.firewall == 'OK') { y.push("Firewall" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Firewall" + ' - <span style=color:red>' + "PROBLEM" + '</span>'); } }
                    x += addDeviceAttribute("Windows Sicherheit", 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>' + "aus" + '</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>' + "aus" + '</span>'); } }
                    if (node.defender.AntivirusSignatureVersion != null) { y.push("Signaturversion" + ' - <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("Antivirus" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Antivirus" + ' - <span style=color:red>' + "PROBLEM" + '</span>'); } }
                    if (node.lsc.firewall != null) { if (node.lsc.firewall == 'OK') { y.push("Firewall" + ' - <span style=color:green>' + "OK" + '</span>'); } else { y.push("Firewall" + ' - <span style=color:red>' + "PROBLEM" + '</span>'); } }
                    if (y.length > 0) x += addDeviceAttribute("Linux Sicherheit", 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>' + "Deaktiviert" + '</span>'; }
                            if (node.av[i].updated !== true) { avx += ' - <span style=color:red>' + "Veraltet" + '</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 + ' - ' + "Gesperrt" : "Gesperrt", true); }
                        if (label) { return '<span style=cursor:default title=\'' + label + '\'>' + escapedUser + '</span>'; }
                        return escapedUser;
                    }).join(', ');
                    x += addDeviceAttribute((node.users.length > 1 ? "Aktive Benutzer" : "Aktiver Nutzer"), u);
                }

                // Windows Idle Time
                if (node.idletime && node.idletime != -1) { x += addDeviceAttribute("Leerlaufzeit", 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("Desktop-Einverständnis & Datenschutzleiste"); } else if (consent & 0x0040) { meshFeatures.push("Desktop-Werkzeugleiste"); } else if (consent & 0x0008) { meshFeatures.push("Desktop-Einverständnis"); } else { if (consent & 0x0001) { meshFeatures.push("Desktop-Benachrichtigung"); } }
                    if (consent & 0x0010) { meshFeatures.push("Terminal-Einverständnis"); } else { if (consent & 0x0002) { meshFeatures.push("Terminal-Benachrichtigung"); } }
                    if (consent & 0x0020) { meshFeatures.push("Dateien-Einverständnis"); } else { if (consent & 0x0004) { meshFeatures.push("Dateien-Benachrichtigung"); } }
                    if (consent == 7) { meshFeatures = ["Immer benachrichtigen"]; }
                    if ((consent & 56) == 56) { meshFeatures = ["Immer nachfragen"]; }

                    meshFeatures = meshFeatures.join(', ');
                    if (meshFeatures == '') { meshFeatures = '<i>' + "Kein(e)" + '</i>'; }
                    x += addDeviceAttribute("Benutzereinwilligung", 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("Verbinden"); }
                    if (devNotify & 4) { devNotifyStr.push("Trennen"); }
                    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("E-Mail ({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("Benachrichtigung ({0})", xx)); }
                    }
                }
                devNotifyStr = devNotifyStr.join(', ');
                if (devNotifyStr == '') { devNotifyStr = '<i>' + "Kein(e)" + '</i>'; }
                x += addDeviceAttribute("Benachrichtigungen", 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-Agent ist verbunden und bereit." + '">' + "Mesh-Agent" + '</span>');
                    if ((node.conn & 2) != 0) cstate.push('<span title="' + "Intel&reg; AMT CIRA ist verbunden und einsatzbereit." + '">' + "Intel&reg; AMT CIRA" + '</span>');
                    else if ((node.conn & 4) != 0) cstate.push('<span title="' + "Intel&reg; AMT ist routebar und einsatzbereit." + '">' + "Intel&reg; AMT" + '</span>');
                    if ((node.conn & 8) != 0) cstate.push('<span title="' + "Mesh-Agent ist erreichbar mittels eines anderen Agents als Vermittler." + '">' + "Mesh Relay" + '</span>');
                    if ((node.conn & 16) != 0) { cstate.push('<span title="' + "MQTT-Verbindung zum Gerät ist aktiv." + '">' + "MQTT" + '</span>'); }
                    x += addDeviceAttribute("Konnektivität", cstate.join(', '));
                }

                // Node tags
                var groupingTags = '<i>' + "Kein(e)" + '</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("Tags", '<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("Tags", '<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-Benutzer+Passwort" : ((node.ssh == 2) ? "SSH-Benutzername+Schlüssel+Passwort" : "SSH-Benutzername+Schlüssel")) + ' <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-Benutzer+Passwort" : ((node.ssh == 2) ? "SSH-Benutzername+Schlüssel+Passwort" : "SSH-Benutzername+Schlüssel"))); }
                        if (node.rdp != null) { y.push("RDP"); }
                    }
                    x += addDeviceAttribute("Anmeldedaten", 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="' + "Gerätegruppen, für die dieses Gerät ein Relais ist" + '">' + "Relais für" + '</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="' + "Aktionen" + '" title="' + "Energiezustandsaktionen auf dem Gerät ausführen" + '" onclick=deviceActionFunction() />'; }
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Notizen" + '" title="' + "Zeige Notizen zu diesem Gerät" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
                x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Ereignis eintragen" + '" title="' + "Schreibe ein Ereignis für dieses Gerät" + '" 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="' + "Ausführen" + '" title="' + "Führen Sie Befehle auf diesem Gerät aus." + '" 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="' + "Nachricht" + '" title="' + "Zeigen Sie eine Textnachricht auf dem Remote-Gerät an" + '" 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="' + "Chat-Fenster mit diesem Computer öffnen" + '" 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="' + "Teilen" + '" title="' + "Erstellen Sie einen Link, um dieses Gerät für einen Gast freizugeben" + '" 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="' + "Schalte aus" + '" title="' + "Schalte aus" + '" 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="' + "Anmachen" + '" title="' + "Anmachen" + '" onclick=setIpPduState(1) />'; }
                        }
                    } else {
                        if (meshrights & 8) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Fernsteuerung" + '" title="' + "Fernsteuerung" + '" 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="' + "Dieses Gerät in eine andere Gerätegruppe verschieben" + '">' + "Gruppe wechseln" + '</a>';
                }
                if ((meshrights & 0x8000) != 0) { // MESHRIGHT_UNINSTALL
                    x += '&nbsp;<a href=# onclick=p10showDeleteNodeDialog("' + node._id + '") title="' + "Dieses Gerät entfernen" + '">' + "Gerät löschen" + '</a>';
                }
                x += '</div><div class="p10html3left">';
                if ((node.agent) && (node.mtype != 3) && ((meshrights & 1048576) != 0)) x += '<a href=# onclick=p10showNodeNetInfoDialog("' + node._id + '") title="' + "Geräte-Netzwerkschnittstelleninformationen anzeigen" + '">' + "Schnittstellen" + '</a>&nbsp;';
                if ((features & 0x00008000) && (xxmap != null)) x += '<a href=# onclick=p10showNodeLocationDialog("' + node._id + '") title="' + "Geräte-Standortinformationen anzeigen" + '">' + "Standort" + '</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="' + "Trafficrouter verwendet, um zu einem Gerät über diesen Server zu verbinden" + '.">' + "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="' + "Öffnen Sie das XTerm-Terminal" + '">' + "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="' + "Erfordert die Installation von MeshCentral Router" + '.">' + "RDP" + ((node.rdpport && (node.rdpport != 3389)) ? '/' + node.rdpport : '') + '</a>&nbsp;';
                                }
                            }
                        }
                        if (node.agent.id > 4) {
                            if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
                                    x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ') title="' + "Erfordert die Installation von MeshCentral Router." + '">' + "SSH" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
                                }
                            }
                            if (navigator.platform.toLowerCase() == 'win32') {
                                if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
                                    x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ') title="' + "Erfordert die Installation von MeshCentral Router." + '">' + "SCP" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
                                }
                            }
                        }
                        if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
                            if ((serverinfo.devicemeshrouterlinks != null) && (Array.isArray(serverinfo.devicemeshrouterlinks.extralinks))) {
                                for (var i in serverinfo.devicemeshrouterlinks.extralinks) {
                                    var r = serverinfo.devicemeshrouterlinks.extralinks[i], p = '\"' + r.protocol + '\"';;
                                    if (doesDeviceMatchFilterTags(node, r.filter)) {
                                        if (typeof r.protocol == 'number') { p = r.protocol; } else if (r.protocol == 'http') { p = 1; } else if (r.protocol == 'https') { p = 2; } else if (r.protocol == 'rdp') { p = 3; } else if (r.protocol == 'ssh') { p = 4; } else if (r.protocol == 'scp') { p = 5; } else if (r.protocol == 'mcrdesktop') { p = 6; } else if (r.protocol == 'mcrfiles') { p = 7; }
                                        if (((p == 6) || (p == 7)) && (node.mtype != 2)) continue; // If this is not an agent device, don't show MeshCentral Router desktop/file links.
                                        x += '<a href=# onclick=p10MCRouter("' + node._id + '",' + p + ',' + r.port + (r.ip ? (',\"' + r.ip + '\"') : ',null') + ',' + (r.localport ? r.localport : 0) + ') title="' + "Erfordert die Installation von MeshCentral Router." + '">' + r.name + '</a>&nbsp;';
                                    }
                                }
                            }
                        }
                    }

                    // noVNC link
                    if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && ((features & 0x20000000) == 0)) {
                        x += '<a href=# cmenu=rfbPortContextMenu id=rfbLink onclick=p10rfb("' + node._id + '") title="' + "Webbasierte VNC-Sitzung mit diesem Gerät starten" + '.">' + "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="' + "Webbasierte RDP-Sitzung mit diesem Gerät starten" + '.">' + "Web-RDP" + '</a>&nbsp;';
                    }

                    // SSH link
                    if ((features2 & 0x200) && (((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) {
                        x += '<a href=# cmenu=sshPortContextMenu id=sshLink onclick=p10ssh("' + node._id + '") title="' + "Webbasierte SSH-Sitzung mit diesem Gerät starten" + '.">' + "Web-SSH" + '</a>&nbsp;';
                    }

                    // MQTT options
                    if ((meshrights == 0xFFFFFFFF) && (features & 0x00400000)) { x += '<a href=# onclick=p10showMqttLoginDialog("' + node._id + '") title="' + "MQTT-Zugangsdaten für dieses Gerät abrufen." + '">' + "MQTT-Anmeldung" + '</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="' + "Der Agent wird auf einem Remote-Gerät mit reduzierten Berechtigungen ausgeführt." + '">' + "Beschränkt" + '</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="' + "Switch-Port verbunden" + '">' + "Switch-Port verbunden" + '</span>' + agentPrivilages;
                        } else {
                            powerstate += '<span style=font-size:12px title="' + "IP-KVM-Port verbunden" + '">' + "IP-KVM-Port verbunden" + '</span>' + agentPrivilages;
                        }
                    } else {
                        powerstate += '<span style=font-size:12px title="' + "Agent verbunden" + '">' + "Agent verbunden" + '</span>' + agentPrivilages;
                    }
                }
                if ((connectivity & 2) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT verbunden" + '">' + "Intel&reg; AMT verbunden" + '</span>'; }
                else if ((connectivity & 4) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "Intel&reg; AMT erkannt" + '">' + "Intel&reg; AMT erkannt" + '</span>'; }
                if ((connectivity & 16) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px title="' + "MQTT verbunden" + '">' + "MQTT-Kanal verbunden" + '</span>'; }
                if ((powerstate == '') && node.lastconnect) { powerstate = '<span style=font-size:12px>' + "Zuletzt gesehen:" + '<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> ' + "Benutzer hinzufügen" + '</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> ' + "Benutzergruppe hinzufügen" + '</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>' + "Benutzerberechtigungen" + '</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="' + "Benutzergruppenrechte an dieser Gerätegruppe entfernen" + '" 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="' + "Entziehe Benutzerrechte an dieser Gerätegruppe" + '" 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 ? "Benutzergruppe" : "Benutzer") + '" 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>' + "Keine Benutzer mit speziellen Geräteberechtigungen" + '</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>' + "Aktive Gerätefreigabe" + '</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="' + "Link zur Gerätefreigabe" + '" 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="' + "Entfernen Sie die Gerätefreigabe" + '" class="fa-fw fa-solid fa-trash text-danger"></i>';
                        var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Dateien", "Terminal + Dateien", "Desktop + Dateien", "Desktop + Terminal + Dateien"][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} bis {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} vor {2} Minute", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            } else {
                                details = format("{0}, {1} vor {2} Minuten", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                            }
                        }
                        if (dshare.recurring == 1) { details += ", täglich wiederkehrend"; }
                        if (dshare.recurring == 2) { details += ", wöchentlich wiederkehrend"; }
                        if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Nur Desktop anzeigen"; }
                        if (dshare.consent != null) {
                            if (dshare.consent == 0) { details += ", keine Zustimmung"; } else {
                                if ((dshare.consent & 0x0038) != 0) { details += "Aufforderung zur Zustimmung"; }
                                if ((dshare.consent & 0x0040) != 0) { details += ", Symbolleiste"; }
                            }
                        }
                        var guestName = EscapeHtml(dshare.guestName);
                        if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Agent-Selbstfreigabe" + '</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', "Leistungsbetrieb", 'Perform power off?');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', function () { meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: 2 }); });
            } else {
                // Turn on
                setModalContent('xxAddAgent', "Leistungsbetrieb", '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">' + "Webseiten Benachrichtigung" + '</div>';
            x += '<div><label><input id=p20notifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Geräteverbindungen" + '</label></div>';
            x += '<div><label><input id=p20notifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Gerätetrennungen" + '</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 Desktop- und Seriell-Ereignisse" + '</label></div>'; }
            if (fx & 1) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "E-Mail Benachrichtigungen" + '</div>';
                x += '<div><label><input id=p20enotifyIntelDeviceConnect type=checkbox class="form-check-input me-2" />' + "Geräteverbindungen" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Gerätetrennungen" + '</label></div>';
                x += '<div><label><input id=p20enotifyIntelDeviceHelp type=checkbox class="form-check-input me-2" />' + "Hilfeanforderungen" + '</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" />' + "Geräteverbindungen" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceDisconnect type=checkbox class="form-check-input me-2" />' + "Gerätetrennungen" + '</label></div>';
                x += '<div><label><input id=p20emsgDeviceHelp type=checkbox class="form-check-input me-2" />' + "Hilfeanforderungen" + '</label></div>';
            }
            setModalContent('xxAddAgent', "Benachrichtungseinstellungen", 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 "Volle Geräterechte"; }
            var str = [];
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Keine Eingabe");
                if (rights & 512) str1.push("Kein Terminal");
                if (rights & 1024) str1.push("Keine Dateien");
                if (rights & 2048) str1.push("Kein AMT");
                if (rights & 4096) str1.push("Eingeschränkte Eingabe");
                if (rights & 65536) str1.push("Kein Desktop");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Mit Gast teilen");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Steuerung"); }
            }
            if (rights & 16) str.push("Konsole");
            if (rights & 32) str.push("Server-Dateien");
            if (rights & 64) str.push("Aufwachen");
            if (rights & 128) str.push("Notizen");
            if (rights & 8192) str.push("Ereignisse begrenzen");
            if (rights & 16384) str.push("Chat");
            if (rights & 32768) str.push("Deinstallation");
            if (rights & 131072) str.push("Befehle");
            if (rights & 262144) str.push("Zurücksetzen / Aus");
            if (rights & 524288) str.push("Teilen");
            if (rights & 1048576) str.push("Details");
            if (rights & 2097152) str.push("Weiterleitung");
            if (str.length == 0) return "Keine Berechtigungen";
            return str.join(', ');
        }

        function makeDeviceGroupRightsString(rights) {
            if (rights == 0xFFFFFFFF) { return "Volle Rechte"; }
            var str = [];
            if (rights & 1) str.push("Gruppe bearbeiten");
            if (rights & 2) str.push("Benutzer verwalten");
            if (rights & 4) str.push("Geräte verwalten");
            if (rights & 8) {
                var str1 = [];
                if (rights & 256) str1.push("Keine Eingabe");
                if (rights & 512) str1.push("Kein Terminal");
                if (rights & 1024) str1.push("Keine Dateien");
                if (rights & 2048) str1.push("Kein AMT");
                if (rights & 4096) str1.push("Eingeschränkte Eingabe");
                if (rights & 65536) str1.push("Kein Desktop");
                if ((rights & 524288) && (serverinfo.guestdevicesharing !== false)) str1.push("Mit Gast teilen");
                if (str1.length > 0) { str.push('Control (' + str1.join(', ') + ')'); } else { str.push("Steuerung"); }
            }
            if (rights & 16) str.push("Konsole");
            if (rights & 32) str.push("Server-Dateien");
            if (rights & 64) str.push("Aufwachen");
            if (rights & 128) str.push("Notizen");
            if (rights & 8192) str.push("Ereignisse begrenzen");
            if (rights & 16384) str.push("Chat");
            if (rights & 32768) str.push("Deinstallation");
            if (rights & 131072) str.push("Befehle");
            if (rights & 262144) str.push("Zurücksetzen / Aus");
            if (rights & 524288) str.push("Teilen");
            if (rights & 1048576) str.push("Details");
            if (rights & 2097152) str.push("Weiterleitung");
            if (str.length == 0) return "Keine Berechtigungen";
            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', "Geräte-Ereignis hinzufügen", '<textarea id=d2devEvent style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea><span style=font-size:10px>' + "Dadurch wird ein Eintrag zum Ereignisprotokoll dieses Geräts hinzugefügt." + '<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>' + "Gerätegruppennotizen können von anderen Gerätegruppenadministratoren eingesehen und verändert werden." + '<span>'; }
            if (showNotesPanel === 'true') { x += ' <span style=font-size:10px><a target=_blank href=\'https://www.markdownguide.org/cheat-sheet/\'>' + "Markdown-Syntax unterstützt" + '</a></span>'; }
            setModalContent('xxAddAgent', "Notizen", 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', "Öffne Seite auf Gerät", '<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>' + "Zeigen Sie ein Meldungsfeld auf dem Remote-Gerät an." + '</div>';
            x += '<textarea id=d2devMessage style=width:100%;height:80px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px></textarea>';
            x += '<select style=width:100% id=d2devTimeout>';
            x += '<option value=2 selected>' + "2 Minuten lang anzeigen (Standard)" + '</option>';
            x += '<option value=10>' + "10 Minuten lang anzeigen" + '</option>';
            x += '<option value=30>' + "30 Minuten lang anzeigen" + '</option>';
            x += '<option value=60>' + "60 Minuten lang anzeigen" + '</option>';
            x += '<option value=0>' + "Nachricht anzeigen, bis sie vom Benutzer geschlossen wird" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Gerätemeldung", 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', "Remote-Eingangssperre", (value == 1) ? "Maus und Tastatur des Remote-Benutzers sperren?" : "Maus und Tastatur des Remote-Benutzers entsperren?");
            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>' + "Bildschirmtastatur"
                            + ' <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',         "Vollbild beenden",                          'deskToggleFull();deskCloseMobileActions()',                       'text-danger') +
                monitorRow +
                row(cls, (_mobileZoomMode==='native' ? 'fa-compress-arrows-alt' : 'fa-expand-arrows-alt'),
                    (_mobileZoomMode==='native' ? "An Fenstergröße anpassen" : "Originalgröße (scrollbar)"),  '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',          "Bildschirmtastatur" + ' <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 ? "Touchpad-Modus deaktivieren" : "Touchpad-Modus aktivieren"),  'deskToggleTrackpad();deskCloseMobileActions()') +
                row(cls, 'fa-hand-pointer',      (swap ? "Rechtsklick-Modus deaktivieren" : "Rechtsklick-Modus aktivieren"),   'deskToggleRightClickMode();deskCloseMobileActions()') +
                row(cls, 'fa-font',              "Text eingeben",                                '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',           "Tastaturkürzel",                       'deskCustomizeKeys();deskCloseMobileActions()') +
                row(cls, 'fa-paste',             "Zwischenablage",                               'showDeskClip();deskCloseMobileActions()') +
                row(cls, 'fa-clipboard',         "Zwischenablage hochladen",                         'deskClipboardOutFunction();deskCloseMobileActions()') +
                row(cls, 'fa-rotate-right',      "Nach rechts drehen",                             'drotate(1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate-left',       "Nach links drehen",                              'drotate(-1);deskCloseMobileActions()') +
                row(cls, 'fa-rotate',            "Desktop aktualisieren",                          'deskRefreshFunction();deskCloseMobileActions()') +
                row(cls, 'fa-gear',              "Desktop-Einstellungen",                         'showDesktopSettings();deskCloseMobileActions()') +
                row(end, 'fa-plug-circle-xmark', "Trennen",                               '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>' + "Toastbenachrichtigung" + '</option><option value=1>' + "Nachrichtenbox" + '</option><option value=3>' + "Alert Box" + '</option></select>';
            x += '<input id=dp2notifyTitle maxlength=256 placeholder="' + "Titel" + '" style=width:100%;box-sizing:border-box;margin-bottom:4px class=form-control/>';
            x += '<textarea id=d2notifyMsg style=background-color:#fcf3cf;width:100%;height:140px;resize:none;overflow-y:scroll;box-sizing:border-box;margin-bottom:4px class=form-control></textarea>';
            x += '<select style=width:100% id=d2notifyTimeout class=form-select>';
            x += '<option disabled value="">Only Applicable to Message Box Notifications</option>';
            x += '<option value=2 selected>' + "2 Minuten lang anzeigen (Standard)" + '</option>';
            x += '<option value=10>' + "10 Minuten lang anzeigen" + '</option>';
            x += '<option value=30>' + "30 Minuten lang anzeigen" + '</option>';
            x += '<option value=60>' + "60 Minuten lang anzeigen" + '</option>';
            x += '<option value=0>' + "Nachricht anzeigen, bis sie vom Benutzer geschlossen wird" + '</option>';
            x += '</select>';
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Gerätebenachrichtigung", 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', "Desktop sperren", "Benutzerdesktop sperren?");
                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 = "Erstellt einen Link, über den ein Gast ohne Konto dieses Gerät für eine begrenzte Zeit fernsteuern kann." + '<br /><br />';
            x += addHtmlFormFloating("Gastname", '<input id=d2inviteName class="form-control" maxlength=128 type=text onkeyup=showShareDeviceValidate() />');
            var deskFull = '<option value=2>' + "Desktop" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x100) != 0)) { deskFull = ''; }
            var fullTerm = '<option value=1>' + "Terminal" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x200) != 0)) { fullTerm = ''; }
            var fullFiles = '<option value=4>' + "Dateien" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x400) != 0)) { fullFiles = ''; }
            var deskFiles = '<option value=5>' + "Desktop + Dateien" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x500) != 0)) { deskFiles = ''; }
            var termFiles = '<option value=6>' + "Terminal + Dateien" + '</option>';
            if ((rights != 0xFFFFFFFF) && ((rights & 0x600) != 0)) { termFiles = ''; }
            var allFeatures = '<option value=7>' + "Desktop + Terminal + Dateien" + '</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>' + "Desktop, nur Ansehen" + '</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("Modus", '<select id=d2shareType class="form-select" onchange=showShareDeviceValidate()>' + y + '</select>');
            var options = { 1: "1 Minute", 5: "5 Minuten", 10: "10 Minuten", 15: "15 Minuten", 30: "30 Minuten", 45: "45 Minuten", 60: "60 Minuten", 120: "2 Stunden", 240: "4 Stunden", 480: "8 Stunden", 720: "12 Stunden", 960: "16 Stunden", 1440: "24 Stunden", 2880: "2 Tage", 5760: "4 Tage", 0: "Unbegrenzt" }
            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("Gültigkeit", '<select id=d2timeRange class="form-select" onchange=showShareDeviceValidate()><option value=0>' + "Jetzt beginnend" + '</option><option value=1>' + "Zeitspanne" + '</option><option value=2>' + "Täglich wiederkehrend" + '</option><option value=3>' + "Wöchentlich wiederkehrend" + '</option></select>');
            x += '<div id=d2modenow>';
            x += addHtmlFormFloating("Ablaufzeit", '<select id=d2inviteExpire class="form-select">' + y + '</select>');
            x += '</div><div id=d2moderange style=display:none>';
            x += addHtmlFormFloating("Zeitbereich Start", '<input id=d2timeRangeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Datum & Uhrzeit auswählen..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Zeitbereich Ende", '<input id=d2timeRangeEndSelector class="flatpickr form-select" type="text" placeholder="' + "Datum & Uhrzeit auswählen..." + '" data-id="altinput">');
            x += '</div><div id=d2moderecurring style=display:none>';
            x += addHtmlFormFloating("Startzeit", '<input id=d2timeStartSelector class="flatpickr form-select" type="text" placeholder="' + "Datum & Uhrzeit auswählen..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Dauer", '<select id=d2inviteDuration class="form-select">' + z + '</select>');
            x += '</div>';
            if (currentNode.agent.caps & 1) { x += '<div id=d2userConsentSelector>' + addHtmlFormFloating("Benutzereinwilligung", '<select id=d2userConsent class="form-select"><option value=0>' + "Nur benachrichtigen" + '<option value=1>' + "Aufforderung zur Zustimmung" + '</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', "Gerät freigeben", 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 = "Wählen Sie eine Operation für die Durchführung auf diesem Gerät." + '<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>' + "Blitz" + '</option>';
                    y += '<option value=401>' + "Vibrieren" + '</option>';
                    z += '<div id=d2devicetimediv>' + addHtmlFormFloating("Zeit", '<select id=d2devicetime class=form-select><option value=1000>' + "1 Sekunde" + '</option><option value=5000>' + "5 Sekunden" + '</option><option value=10000>' + "10 Sekunden" + '</option></select>') + '</div>';
                }
            } else {
                if ((rights & 64) != 0) { count++; y += '<option value=100>' + "Aufwecken" + '</option>'; } // Wake-up permission
                if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Befehle ausführen" + '</option>'; } // Remote command permission
                if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Ruhezustand" + '</option><option value=3>' + "Neustarten" + '</option><option value=2>' + "Ausschalten" + '</option>'; }
                if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Sende MQTT-Nachricht" + '</option>'; }
                if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
                    count++;
                    y += '<option value=310>' + "Intel&reg; AMT Zurücksetzen" + '</option>';
                    y += '<option value=308>' + "Intel&reg; AMT Ausschalten" + '</option>';
                    if ((xxcurrentView == 11) || (xxcurrentView == 12)) { // Only show these options on terminal or desktop tabs
                        y += '<option value=312>' + "Intel&reg; AMT Reset ins BIOS" + '</option>';
                        y += '<option value=311>' + "Intel&reg; AMT Einschalten ins 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 Einschalten" + '</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 mit einem Klick Wiederherstellen" + '</option>'; } // CIRA (2) or AMT (4) connected
                if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Agent deinstallieren" + '</option>'; }
            }
            y += '</select>';
            x += addHtmlFormFloating("Vorgang", y);
            if (count == 0) { x = "Derzeit sind keine Aktionen für dieses Gerät verfügbar."; }
            xxdialogButtons = (count == 0) ? 2 : 3; xxdialogTag = x + z;
            setModalContent('xxAddAgent', "Geräteaktion", 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 = "Führen Sie Befehle auf ausgewählten Geräten aus." + '<br />';
                    if (wintype == true) {
                        x += '<select id=d2cmdtype style=width:100%;margin-bottom:4px;margin-top:4px> class=form-select>';
                        x += '<option value=1>' + "Windows-Eingabeaufforderung" + '</option><option value=2>' + "Windows PowerShell" + '</option>';
                        if (linuxtype == true) { x += '<option value=3>' + "Linux / BSD / macOS-Befehlsshell" + '</option>'; }
                        x += '</select>';
                    }
                    x += '<select id=d2cmduser style=width:100%;margin-bottom:4px><option value=0>' + "Als Agent ausführen" + '</option><option value=1>' + "Als Benutzer ausführen, Agent, wenn kein Benutzer vorhanden ist" + '</option><option value=2>' + "Muss als Benutzer ausgeführt werden" + '</option></select>';
                    x += '<textarea id=d2runcmd style=width:100%;height:200px;resize:none;overflow-y:scroll></textarea>';
                    setModalContent('xxAddAgent', "Befehle ausführen", 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 mit einem Klick Wiederherstellen", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deviceActionOneClickRecovery());
                d3init();
            } else if (op == 302) { // Intel AMT power on
                setModalContent('xxAddAgent', "Intel&reg; AMT in Betrieb", "Intel&reg; AMT Einschalten?");
                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 in Betrieb", "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 in Betrieb", "Intel&reg; AMT Zurücksetzen?");
                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 in Betrieb", "Intel&reg; AMT Einschalten ins BIOS durchführen?");
                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 in Betrieb", "Intel&reg; AMT Reset ins BIOS durchführen?");
                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} von {1} bis {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>' + "Tag" + '</th><th scope=col>' + "7-Tage-Energiezustand" + '<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="' + "Energiezustandsereignisse herunterladen" + '" 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("Benutzername", '<input id=dp10username class="form-control" maxlength=32 autocomplete=nope placeholder="admin" onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            x += addHtmlFormFloating("Passwort", '<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("Sicherheit", '<select id=dp10tls class="form-select"><option value=0>' + "Keine TLS-Sicherheit" + '</option><option value=1>' + "TLS-Sicherheit vorausgesetzt" + '</option></select>'); }
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { buttons = 7; }
            setModalContent('xxAddAgent', "Zugangsdaten für Intel&reg; AMT bearbeiten", 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("Thema", '<input id=dp2topic class="form-control" maxlength=64 onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1) />');
            x += addHtmlFormFloating("Nachricht", '<textarea id=dp2msg class="form-control" maxlength=4096 style=width:100%;height:150px;resize:none onchange=p10validateSendMqttMsgDialog() onkeyup=p10validateSendMqttMsgDialog(event,1)></textarea>');
            setModalContent('xxAddAgent', "Sende MQTT-Nachricht", 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("Möchten Sie die ausgewählten {0} Agent wirklich deinstallieren?", nodeids.length); } else { x = "Möchten Sie den ausgewählten Agent wirklich deinstallieren?"; }
            x += '<br /><br />';
            if (nodeids.length > 1) { x += "Dies wird die Geräte nicht vom Server entfernen, aber die Geräte werden nicht länger eine Verbindung zum Server aufbauen können. Aller entfernter Zugriff auf die Geräte geht verloren. Die Geräte müssen verbunden sein, damit dieser Befehl funktioniert."; } else { x += "Dies wird das Gerät nicht vom Server entfernen, aber das Gerät wird nicht länger eine Verbindung zum Server aufbauen können. Aller entfernter Zugriff auf das Gerät geht verloren. Das Gerät muss verbunden sein, damit dieser Befehl funktioniert."; }
            x += '<br /><br /><label style=color:red><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Bestätigen" + '</label>';
            setModalContent('xxAddAgent', "Agent deinstallieren", 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) ? ("Wähle eine neue Gruppe für dieses Gerät" + '<br /><br />') : ("Wähle eine neue Gruppe für die ausgewählten Geräte" + '<br /><br />');
                x += addHtmlFormFloating("Neue Gerätegruppe", y);
                setModalContent('xxAddAgent', "Gruppe wechseln", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p10showChangeGroupDialogEx(3, nodeids));
            } else {
                setModalContent('xxAddAgent', "Gruppe wechseln", '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("Möchten Sie den Knoten {0} wirklich löschen?", EscapeHtml(currentNode.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Bestätigen" + '</label>';
            xxdialogButtons = 3; xxdialogTag = nodeid;
            setModalContent('xxAddAgent', "Knoten löschen", 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', "Gerätestandort", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            return false;
        }

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

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

        // Show MeshCentral Router dialog
        function p10showMeshRouterDialog() {
            if (xxdialogMode) return;
            var x = '<div>' + "MeshCentral Router ist ein Windows-Werkzeug für die TCP-Portweiterleitung. Sie können beispielsweise über diesen Server ein Remote-Gerät per RDP erreichen." + '</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">' + "Führen Sie MeshCentral Router aus und klicken Sie auf \"Installieren\", um es über den Browser startbar zu machen." + '</div>';
            x += '<br /><a style=cursor:pointer target="mcrouterframe" rel="noreferrer noopener" href="' + url + '"><input type=button style=width:100%;cursor:pointer value="' + "MeshCentral Router starten" + '" class="btn btn-primary btn-block" /></a>';
            x += '<iframe class="d-none" name="mcrouterframe"></iframe>';
            setModalContent('xxAddAgent', "MeshCentral Router", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p20showAddMeshUserDialogEx(1, 'fileDownload');
            });
        }

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

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

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

            var x = '';
            if (mode == 0) { x += '<div>' + "MeshCmd ist ein Befehlszeilenprogramm, das viele verschiedene Vorgänge ausführt. Die Aktionsdatei kann optional heruntergeladen und bearbeitet werden, um Serverinformationen und Anmeldeinformationen bereitzustellen." + '<br /><br />'; }
            if (mode == 1) { x += '<div>' + "Laden Sie \"meshcmd\" mit einer Aktionsdatei herunter, um den Datenverkehr über diesen Server an dieses Gerät weiterzuleiten. Stellen Sie sicher, dass Sie meshaction.txt bearbeiten und Ihr Kontokennwort hinzufügen, oder nehmen Sie die erforderlichen Änderungen vor." + '<br /><br />'; }
            x += addHtmlFormFloating("Betriebssystem", 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("Aktionsdatei", '<a onclick=downloadFile("meshagents?meshaction=generic' + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary>' + "MeshAction (.txt)" + '</a>'); }
            if (mode == 1) { x += addHtmlValue("Aktionsdatei", '<a onclick=downloadFile("meshagents?meshaction=route&nodeid=' + nodeid + (urlargs.key ? ('&key=' + urlargs.key) : '') + '") class=link-primary>' + "MeshAction (.txt)" + '</a>'); }
            x += '</div>';
            xxdialogButtons = 9; xxdialogMode = 'fileDownload';
            setModalContent('xxAddAgent', "MeshCmd herunterladen", 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 ausführbare Datei)"; }
            if (os == 4) { osn = "MeshCmd (Win x86-64 ausführbare Datei)"; }
            if (os == 43) { osn = "MeshCmd (Win ARM-64 ausführbare Datei)"; }
            if (os == 5) { osn = "MeshCmd (Linux x86, 32bit)"; }
            if (os == 6) { osn = "MeshCmd (Linux x86, 64bit)"; }
            if (os == 16) { osn = "MeshCmd (macOS, x86-64bit)"; }
            if (os == 29) { osn = "MeshCmd (macOS, ARM-64bit)"; }
            if (os == 25) { osn = "MeshCmd (Linux ARM, 32bit)"; }
            if (os == 26) { osn = "MeshCmd (Linux ARM, 64bit)"; }
            QH('meshcmddownloadid', osn);
            Q('meshcmddownloadid').onclick = function () { downloadFile('meshagents?meshcmd=' + os + (urlargs.key ? ('&key=' + urlargs.key) : '')); }
        }

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

            var x = '<div style=text-align:center><br />';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i1 onclick=p10setIcon(1) onkeypress="if (event.key==\'Enter\') p10setIcon(1)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i2 onclick=p10setIcon(2) onkeypress="if (event.key==\'Enter\') p10setIcon(2)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i3 onclick=p10setIcon(3) onkeypress="if (event.key==\'Enter\') p10setIcon(3)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i4 onclick=p10setIcon(4) onkeypress="if (event.key==\'Enter\') p10setIcon(4)"></div>';
            x += '<br />';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i5 onclick=p10setIcon(5) onkeypress="if (event.key==\'Enter\') p10setIcon(5)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i6 onclick=p10setIcon(6) onkeypress="if (event.key==\'Enter\') p10setIcon(6)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i7 onclick=p10setIcon(7) onkeypress="if (event.key==\'Enter\') p10setIcon(7)"></div>';
            x += '<div tabindex=0 style="display:inline-block;margin:0 10px 0 10px" class=i8 onclick=p10setIcon(8) onkeypress="if (event.key==\'Enter\') p10setIcon(8)"></div>';
            x += '<br /><br /></div>';
            setModalContent('xxAddAgent', "Icon-Auswahl", 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', "Gerät bearbeiten", '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', "Gerät bearbeiten", '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 = ["Gerätename", "Hostname", "Beschreibung", "Tags"];
        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', "Gerät bearbeiten", 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 = ['', "Warten auf den Benutzer, um Zugriff zu gewähren ...", "Verweigert", "Fehler beim Starten der Remote-Terminalsitzung, {0} ({1})", "Auszeit", "Ungültige Netzwerkdaten empfangen", "Anzeige kann nicht erfasst werden", "Protokollverhandlung fehlgeschlagen ({0})", "NLA nicht unterstützt", "SSL vom Server erforderlich", "SSL vom Server nicht zugelassen", "SSL-Zertifikat nicht auf dem Server", "Inkonsistente Flaggen", "Hybrid vom Server erforderlich", "SSL mit Benutzerauthentifizierung vom Server erforderlich"];
        function formatAgentConsoleMessage(msg, msgid, msgargs) {
            var r;
            if (msgargs == null) { msgargs = []; }
            while (msgargs.length < 3) { msgargs.push(''); } // We need to call the format function in a way that works with older browsers and minifier, can't use apply() or ...
            if (msgid && (msgid < agentConsoleMessages.length)) { r = EscapeHtml(format(agentConsoleMessages[msgid], (msgargs[0]), (msgargs[1]), (msgargs[2]))); } else { r = EscapeHtml(msg); }
            return r.split('\n').join('<br />') + '<br /><br />';
        }

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

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

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

        function askRdpCredentials() {
            if (xxdialogMode) return;
            var x = '<div>';
            if (currentNode.rdp == 1) {
                x += addHtmlFormFloating("Anmeldedaten", '<select id=d2mode class="form-select" onchange=askRdpCredentialsValidate()><option value=1>' + "Serveranmeldeinformationen verwenden" + '</option><option value=2>' + "Neue Zugangsdaten verwenden" + '</option></select>');
                x += '<div id=d2cred style=display:none>';
            } else {
                x += '<div id=d2cred>';
            }
            x += addHtmlFormFloating("Domain", '<input type=text id=d2domain class="form-control" maxlength=128 />');
            x += addHtmlFormFloating("Benutzername", '<input type=text id=d2user class="form-control" onKeyUp=askRdpCredentialsValidate() maxlength=128 />');
            x += addHtmlFormFloating("Passwort", '<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>' + "Anmeldedaten merken" + '</label>'); }
            x += '</div>';
            x += MoreStart();
            x += addHtmlFormFloating("Alternative Shell", '<input type=text id=d2altshell class="form-control" maxlength=2048 />');
            x += addHtmlFormFloating("Arbeitsverzeichnis", '<input type=text id=d2workdir class="form-control" maxlength=2048 />');
            x += MoreEnd();
            setModalContent('xxAddAgent', "RDP Anmeldedaten", 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} sehen zu", 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("Startzeit", 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 Verbindung" : format("{0} Verbindungen", conn.metadata.users[i]);
                        x += addHtmlValue2(getUserName(i), val);
                    }
                }
                xxdialogTag = 'sessionMetadata' + cid;
                setModalContent('xxAddAgent', "Sitzungsinformationen", 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 = "Alles fokussieren";
                    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', "Einstellungen des entfernten Desktops", 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 = "Alles fokussieren"; }
                    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 = ["Alles fokussieren", "Kleiner Fokus", "Großer Fokus"][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: "Rücktaste", 9: "Tab", 13: "Eingabe", 27: "Escape", 32: "Space", 44: "Bildschirm Drucken", 45: "Einfügen", 46: "Entf", 36: "Pos1", 35: "Ende", 33: "Seite hoch", 34: "Seite runter", 37: "Links", 38: "Hoch", 39: "Rechts", 40: "Runter", 0: "Kein(e)" }

        function keyShortcutTotext(n) {
            var x = [];
            if (n & 0x010000) { x.push("Umschalten"); }
            if (n & 0x020000) { x.push("Alt"); }
            if (n & 0x080000) { x.push("Strg"); }
            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', "Tastaturkürzel", 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("Name", '<input type=text id=d2shortcutname class="form-control" maxlength=32 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating("Wert", '<input type=text id=d2shortcutvalue class="form-control" maxlength=256 autocomplete=off onkeyup=deskCustomizeStringsUpdate(event) />');
            x += addHtmlFormFloating('', '<input id=d2addbutton type=button value=' + "hinzufügen" + ' onclick=addDeskCustomizeString() class="form-control" />');
            x + '</div>';
            setModalContent('xxAddAgent', "Anpassung der Tastatursaiten", 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>' + "Keine Tastaturzeichenfolgen definiert" + '</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>' + "Geben Sie Text ein und klicken Sie auf OK, um ihn remote einzugeben. Stellen Sie sicher, dass sich der Remote-Cursor an der richtigen Position befindet, bevor Sie fortfahren." + '<div>';
            x += '<textarea id=d2typeText style="margin-top:5px;width:100%;height:184px;resize:none" maxlength=2000>' + (text ? EscapeHtml(text) : '') + '</textarea>';
            setModalContent('xxAddAgent', "Entfernte Tastatureingabe", 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="' + "Zwischenablage holen" + '" onclick=showDeskClipGet()></div>';
            if ((features2 & 0x1000) == 0) x += '<div class="col"><input class="btn btn-secondary" id=dlgClipSet type=button value="' + "Zwischenablage setzen" + '" 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>' + "Entfernte Zwischenablage ist für 60 Sekunden gültig." + '</span></div></div></div>';
            x += '</div>'
            xxdialogTag = 'clipboard';
            setModalContent('xxAddAgent', "Entfernte Zwischenablage", 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="' + "Prozess stoppen" + '" 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("Prozessdetails, #{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 = "gestoppt"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "gestartet"; }
                        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("Name", service.name); }
                    if (service.displayName) { x += addHtmlValue("Anzeigename", service.displayName); }
                    if (service.status) {
                        var ss = capitalizeFirstLetter(service.status.state.toLowerCase());
                        if (ss == 'Stopped') { ss = "gestoppt"; } // TODO: Add all other states for translation
                        else if (ss == 'Running') { ss = "gestartet"; }
                        if (service.status.state) { x += addHtmlValue("Zustand", ss); }
                        if (service.status.pid) { x += addHtmlValue("PID", service.status.pid); }
                        var serviceTypes = [];
                        if (service.status.isFileSystemDriver === true) { serviceTypes.push("Dateisystemtreiber"); }
                        if (service.status.isInteractive === true) { serviceTypes.push("Interaktiv"); }
                        if (service.status.isKernelDriver === true) { serviceTypes.push("Kernel-Treiber"); }
                        if (service.status.isOwnProcess === true) { serviceTypes.push("Eigener Prozess"); }
                        if (service.status.isSharedProcess === true) { serviceTypes.push("Geteilter Prozess"); }
                        if (serviceTypes.length > 0) { x += addHtmlValue("Modus", serviceTypes.join(', ')); }
                    }
                    if (service.startType) { x += addHtmlValue("Start Type", service.startType); }
                    if (service.user) { x += addHtmlValue("Benutzer", 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("Erster Fehlschlag", 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="' + "Schließen" + '" onclick=showServiceDetailsDialogEx(0,' + index + ')></div><div style=margin-bottom:12px><input type=button value="' + "Start" + '" onclick=showServiceDetailsDialogEx(1,' + index + ')><input type=button value="' + "Stop" + '" onclick=showServiceDetailsDialogEx(2,' + index + ')><input type=button value="' + "Neustart" + '" onclick=showServiceDetailsDialogEx(3,' + index + ')></div>';
                    xxdialogTag = "service_" + index;
                    setModalContent('xxAddAgent', "Dienst-Details", 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', "Dienst-Details", '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 = "Desktop" + '-' + 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 = "Alle Bildschirme"; allDisplays = 2; } // Language translation
                if (str.startsWith('Display ')) { str = format("Anzeige {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>' + "Bildschirmtastatur"
                    + ' <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', "Prozesskontrolle", format("Prozess #{0} \"{1}\" stoppen?", 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-Port {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("Authentifizierung", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=3 selected>' + "Gespeicherter Schlüssel" + '</option><option value=1>' + "Benutzername Passwort" + '</option><option value=2>' + "Benutzername und Schlüssel" + '</option></select>');
            } else {
                x += addHtmlFormFloating("Authentifizierung", '<select id=dp2authmethod class="form-select" onchange=sshAuthUpdate(event)><option value=1 selected>' + "Benutzername Passwort" + '</option><option value=2>' + "Benutzername und Schlüssel" + '</option></select>');
            }
            x += '<div id=d2userauth style=display:none>';
            x += addHtmlFormFloating("Benutzername", '<input id=dp2user class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
            x += '</div>';
            x += '<div id=d2passauth style=display:none>';
            x += addHtmlFormFloating("Passwort", '<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">' + "Anmeldedaten merken" + '</label>'); }
            x += '</div><div id=d2keyauth style=display:none>';
            x += addHtmlValue("Schlüsseldatei", '<input type=file id=dp2key class="form-control" maxlength=64 autocomplete=off onchange=sshAuthUpdate(event) />' + '<div id=d2badkey style=font-size:x-small>' + "Die Schlüsseldatei muss im OpenSSH-Format vorliegen." + '</div>');
            x += addHtmlFormFloating("Schlüssel Passwort", '<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)>' + "Benutzer & Schlüssel merken" + '</label>');
                x += addHtmlValue('', '<label><input id=dp2keep2 type=checkbox class="form-check-input me-2">' + "Passwort merken" + '</label>');
            }
            x += '</div>';
            if (j.askkeypass) {
                x += '<div id=d2keyauth2 style=display:none>';
                x += addHtmlFormFloating("Passwort", '<input type=password id=dp2keypass2 class="form-control" maxlength=64 autocomplete=off onkeyup=sshAuthUpdate(event) />');
                x += '</div>';
            }
            setModalContent('xxAddAgent', "Authentifizierung", 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("Verbindungsfehler", 5000); break; }
                        case 'autherror': { p12setConsoleMsg("Authentifizierungsfehler", 5000); break; }
                        case 'sessionerror': { p12setConsoleMsg("Sitzung abgelaufen", 5000); break; }
                        case 'sessiontimeout': { p12setConsoleMsg("Session-Timeout", 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-Terminal", "Erweitertes 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)", "Alternativ (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', "Einfügen", '<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>' + "Keine Dateien gefunden" + '</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("Authentifizierungsfehler", 5000); return; }
                case 'connectionerror': { p13setConsoleMsg("Verbindungsfehler", 5000); return; }
                case 'sessionerror': { p13setConsoleMsg("Sitzung abgelaufen", 5000); return; }
                case 'sessiontimeout': { p13setConsoleMsg("Session-Timeout", 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', "Dateivorgang", '<div style=margin:10px>' + "Dateien komprimieren ..." + '</div>');
                    showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p13fileOperationDialogEx(10));
                } else if ((data.msg == 'unzipping') && ((!xxdialogMode) || (xxdialogTag == 'fileMsgDialog'))) {
                    // Show the dialog box message
                    setModalContent('xxAddAgent', "Dateivorgang", '<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', "Dateivorgang", '<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', "Dateivorgang", '<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', "Datei/Ordner öffnen", "Sind Sie sicher, dass Sie diese Datei/Ordner auf dem Desktop des Remote-Geräts öffnen möchten?");
            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("Filter", '<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=' + "Schließen" + ' style=float:right;width:80px;margin-left:5px onclick=p13findfileCancel()>';
            x += '<input id=filefind_dlgOkButton type=submit value=' + "Suche" + ' style=float:right;width:80px onclick=p13findfileEx()><br /><br /></div>';
            setModalContent('xxAddAgent', "Dateien suchen", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
            p13findfileValidate();
            Q('d2findFilter').focus();
        }

        function p13findfileDown(e) { if (e.code == "Eingabe") { 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 = "Alle auswählen";
                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 ? "Auswahl aufheben" : "Alle auswählen");
                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-Port {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', "Neuer Ordner", '<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', "Neue Datei", '<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>' + "Rekursiv Löschen" + '</label><br>' : '<input type=checkbox class="form-check-input me-2" id=p13recdeleteinput style=\'display:none\'>';
            setModalContent('xxAddAgent', "Löschen", (cc > 1) ? (format("{0} ausgewählte Elemente löschen?", cc) + rec) : ("Ausgewähltes Element löschen?" + 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', "Umbenennen", '<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', "Datei hochladen", '<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("Datei-Editor", "Nur Dateien kleiner als 200 kB können bearbeitet werden."); }
                    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-Dateiname", '<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("Bestätigen Sie das Kopieren von {0} Einträgen an diesen Ort?", p13clipboard.length); } else { x = format("Bestätigen Sie das Kopieren von 1 Eintrag an diesen Ort?"); }
                } else {
                    if (p13clipboard.length > 1) { x = format("Bestätigen Sie das Verschieben von {0} Einträgen an diesen Ort?", p13clipboard.length); } else { x = format("Bestätigen Sie das Verschieben von 1 Eintrag an diesen Ort?"); }
                }
            }
            setModalContent('xxAddAgent', "Einfügen", 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("Halte {0} Einträge zum Kopieren" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Leeren" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Halte 1 Eintrag zum Kopieren" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Leeren" + '</a>.');
                    }
                } else {
                    if (p13clipboard.length > 1) {
                        x = format("Halte {0} Einträge zum Verschieben" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Leeren" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("Halte 1 Eintrag zum Verschieben" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "Leeren" + '</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', "Datei herunterladen", '<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 = ["Umbruch: an", "Umbruch: aus"][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 = ["Größe: 100%", "Größe: 125%", "Größe: 150%", "Größe: 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 = ["Kodierung: RAW", "Kodierung: UTF8"][d4EditEncodingVal];
            putstore('editorEncoding', d4EditEncodingVal);
        }

        function d4ToggleLineBreak(update) {
            if (!update) {
                d4EditLineBreakVal = ++d4EditLineBreakVal % 3;
            }
            Q('d4LineBreakButton').value = ["Zeilenumbruch: Windows (CR LF)", "Zeilenumbruch: Linux (LF)", "Zeilenumbruch: 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', "Datei hochladen", format((overWriteCount == 1) ? "Das Hochladen wird 1 Datei überschreiben. Fortfahren?" : "Das Hochladen wird {0} Dateien überschreiben. Fortfahren?", 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', "Datei hochladen", '<div id=p13dfileName>' + "Verbinden..." + '</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="' + "Dies ist eine Gast-Sharing-Sitzung" + '">' + 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>' + "Keine Ereignisse vorhanden" + '</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', "Ereignisdetails", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton');
            }
        }

        //
        // DEVICE DETAILS
        //

        var deviceDetailsStats = null;
        var deviceDetailsStatsTimer = null;
        var deviceDetailsStatsData = [];
        var deviceDetailsConfig = {
            type: 'line',
            data: {
                labels: [],
                datasets: [
                    { label: "CPU", backgroundColor: 'rgba(134, 16, 158, .5)', borderColor: 'rgb(134, 16, 158)', data: [], fill: true },
                    { label: "Speicher", 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("Name", EscapeHtml(node.rname), s); }
            if (hardware.windows && hardware.windows.osinfo && hardware.windows.osinfo.Description) { x += addDetailItem("Beschreibung", EscapeHtml(hardware.windows.osinfo.Description), s); }
            if (node.osdesc) { x += addDetailItem("Version", EscapeHtml(node.osdesc), s); }
            if (hardware.windows && hardware.windows.osinfo) {
                var m = hardware.windows.osinfo;
				if(m.BuildRevision){ x += addDetailItem("Build-Nummer", EscapeHtml(m.BuildRevision), s); }
                if (m.OSArchitecture) {
                    if (m.OSArchitecture.startsWith('32')) { x += addDetailItem("Architektur", "32-Bit", s); }
                    else if (m.OSArchitecture.startsWith('64')) { x += addDetailItem("Architektur", "64-Bit", s); }
                    else { x += addDetailItem("Architektur", 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("Letzter Startvorgang", date);
                }
                if(m.Domain){ x += addDetailItem((m.PartOfDomain ? "Domain" : "Arbeitsgruppe"), EscapeHtml(m.Domain), s); }
                if(m.DomainState > 0){ x += addDetailItem("Domain-Status", (domainStates[m.DomainState] ? domainStates[m.DomainState] : "Unbekannt"), s); }
            }
            if (hardware.linux && hardware.linux.LastBootUpTime) {
                var lastBootUpTime = new Date(hardware.linux.LastBootUpTime);
                const date = printDateTime(lastBootUpTime);
                x += addDetailItem("Letzter Startvorgang", date);
            }
            if (hardware.linux && hardware.linux.arch) {
                var arch = hardware.linux.arch;
                x += addDetailItem("Architektur", (['x86_64','amd64'].includes(arch) ? "64-Bit" : ['i686','x86','i586'].includes(arch) ? "32-Bit" : EscapeHtml(arch)), s);
            }
            if (hardware.linux && hardware.linux.kernel_release) { x += addDetailItem("Kernel-Version", EscapeHtml(hardware.linux.kernel_release), s); }
            if (hardware.linux && hardware.linux.kernel_build) { x += addDetailItem("Kernel-Build", 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("Letzter Startvorgang", date);
            }
            if (x != '') { sections.push({ name: "Betriebssystem", html: x, img: 'software64.png' }); }

            // MeshAgent
            if (node.agent) {
                var x = '';
                if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
                    var str = '';
                    if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
                    if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
                    if (node.agent.id == 14) { str = node.agent.core; }
                    x += addDetailItem("Mesh-Agent", str);
                }
                if (node.firstconnect) { x += addDetailItem("Erste Agentenverbindung", printDateTime(new Date(node.firstconnect))); }
                if ((node.conn & 1) != 0) {
                    x += addDetailItem("Letzte Agent-Verbindung", "Jetzt verbunden");
                } else {
                    if (node.lastconnect) { x += addDetailItem("Letzte Agent-Verbindung", printDateTime(new Date(node.lastconnect))); }
                }
                if (node.lastaddr) {
                    var splitip = node.lastaddr.split(':');
                    if (splitip.length > 2) {
                        // IPv6
                        x += addDetailItem("Letzte Agent-Adresse", '<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("Letzte Agent-Adresse", splitip[0]);
                        } else {
                            x += addDetailItem("Letzte Agent-Adresse", '<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("Kompilierungszeit", isNaN(d) ? EscapeHtml(hardware.agentvers.compileTime) : printDateTime(new Date(d)));
                    }
                }
                if (hardware.time != null) {
                    x += addDetailItem("Letzte Detail-Aktualisierung", printDateTime(new Date(hardware.time)));
                }
                if (x != '') { sections.push({ name: "Mesh-Agent", html: x, img: 'meshagent64.png' }); }
            }

            // Mobile
            if (hardware.mobile) {
                var x = '';
                if (hardware.mobile.brand && hardware.mobile.model) { x += addDetailItem("Modell", EscapeHtml(hardware.mobile.brand + ', ' + hardware.mobile.model), s); }
                if (hardware.mobile.device) { x += addDetailItem("Gerät", EscapeHtml(hardware.mobile.device), s); }
                if (hardware.mobile.bootloader) { x += addDetailItem("Bootloader", EscapeHtml(hardware.mobile.bootloader), s); }
                if (hardware.mobile.id) { x += addDetailItem("Kennung", EscapeHtml(hardware.mobile.id), s); }
                if (hardware.mobile.host) { x += addDetailItem("Hostname", 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: "Mobiles Gerät", 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("Beschreibung", 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-Schicht", format("MAC: {0}, Gateway: {1}", EscapeHtml(m.mac), EscapeHtml(m.gatewaymac)));
                        } else {
                            if (m.mac) { x += addDetailItem("MAC-Schicht", 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-Schicht", format("IP: {0}, Maske: {1}, Gateway: {2}", EscapeHtml(ipv4layer.v4addr), EscapeHtml(ipv4layer.v4mask), EscapeHtml(ipv4layer.v4gateway)));
                        } else {
                            if (ipv4layer.v4addr) { x += addDetailItem("IPv4-Schicht", format("IP: {0}", EscapeHtml(ipv4layer.v4addr))); }
                        }
                    }
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Netzwerk", 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("Beschreibung", 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-Schicht", format("MAC: {0}, Gateway: {1}", EscapeHtml(m[0].mac), EscapeHtml(m[0].gatewaymac)));
                        } else {
                            x += addDetailItem("MAC-Schicht", format("MAC: {0}", EscapeHtml(m[0].mac)));
                        }
                    }
                    if (typeof m[0].speed == 'number' && (m[0].speed != 9223372036854775807 && m[0].speed > 0)) {
                        x += addDetailItem("Schnittstellengeschwindigkeit", 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("Maske: {0}", EscapeHtml(iplayer.netmask))); }
                        if (iplayer.gateway) { items.push(format("Gateway: {0}", EscapeHtml(iplayer.gateway))); }
                        if (items.length > 0) {
                            if (iplayer.family == 'IPv4') { x += addDetailItem("IPv4-Schicht", items.join(", ")); }
                            if (iplayer.family == 'IPv6') { x += addDetailItem("IPv6-Schicht", 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 Server" + '</b>', EscapeHtml(hardware.network.dns.join(", ")));
                    x += '</div></td></tr>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Netzwerk", html: x, img: 'networking64.png' }); }
            }

            // Attribute: Intel AMT
            if (node.intelamt != null) {
                var x = '';
                x += addDetailItem("Version", (node.intelamt.ver) ? ('v' + EscapeHtml(node.intelamt.ver)) : ('<i>' + "Unbekannt" + '</i>'), s);
                var provisioningStates = { 0: nobreak("Nicht aktiviert"), 1: nobreak("Voraktivierung"), 2: nobreak("Aktiviert") };
                var provisioningMode = '';
                if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Client Control Mode (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Admin Control Mode (ACM)"); } }
                x += addDetailItem("Bereitstellungszustand", ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('<i>' + "Unbekannt" + '</i>')) + provisioningMode, s);
                x += addDetailItem("Sicherheit", (node.intelamt.tls == 1) ? "Mit TLS gesichert" : "TLS ist nicht eingerichtet", s);
                // Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
                x += addDetailItem("Administrator-Anmeldeinformationen", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0))) ? "Nicht bekannt" : "Bekannt", s);
                if (x != '') {
                    if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
                        sections.push({ name: "Intel&reg; Standard-Verwaltbarkeit (Intel&reg; SM)", html: x, img: 'amt64.png' });
                    } else {
                        sections.push({ name: "Intel&reg; Active Management Technology (Intel&reg; AMT)", html: x, img: 'amt64.png' });
                    }
                }
            }

            if (hardware.identifiers) {
                var x = '', ident = hardware.identifiers;
                // BIOS
                if (ident.bios_vendor) { x += addDetailItem("Hersteller", EscapeHtml(ident.bios_vendor), s); }
                if (ident.bios_version) { x += addDetailItem("Version", EscapeHtml(ident.bios_version), s); }
                if (ident.bios_serial) { x += addDetailItem("Seriennummer", 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("Hersteller", EscapeHtml(ident.board_vendor), s); }
                if (ident.board_name) { x += addDetailItem("Name", EscapeHtml(ident.board_name), s); }
                if (ident.board_serial && (ident.board_serial != '')) { x += addDetailItem("Seriennummer", EscapeHtml(ident.board_serial), s); }
                if (ident.board_version) { x += addDetailItem("Version", EscapeHtml(ident.board_version), s); }
                if (ident.product_uuid) { x += addDetailItem("Kennung", EscapeHtml(ident.product_uuid), s); }
                if (ident.cpu_name) { x += addDetailItem("CPU", EscapeHtml(ident.cpu_name).split('(TM)').join('&trade;').split('(R)').join('&reg;'), s); }
                if (ident.gpu_name) { for (var i in ident.gpu_name) { x += addDetailItem("GPU", EscapeHtml(ident.gpu_name[i]).split('(TM)').join('&trade;').split('(R)').join('&reg;'), s); } }
                if (x != '') { sections.push({ name: "Hauptplatine", html: x, img: 'motherboard64.png' }); }

                // System
                x = '';
                if (ident.chassis_manufacturer) { x += addDetailItem("Hersteller", EscapeHtml(ident.chassis_manufacturer), s); }
                if (ident.product_name) { x += addDetailItem("Produktname", EscapeHtml(ident.product_name), s); }
                if (ident.chassis_serial) { x += addDetailItem("Seriennummer", EscapeHtml(ident.chassis_serial), s); }
                if (ident.chassis_assettag) { x += addDetailItem("Anlageetikett", EscapeHtml(ident.chassis_assettag), s); }
                if (x != '') { sections.push({ name: "System", html: x, img: 'system64.png' }); }
            }

            // TPM
            if (hardware.tpm) {
                var x = '', tpm = hardware.tpm;
                if (tpm.SpecVersion) { x += addDetailItem("TPM-Standard", parseFloat(EscapeHtml(tpm.SpecVersion)).toFixed(1), s); }
                if (tpm.ManufacturerId) { x += addDetailItem("Kennung", EscapeHtml(tpm.ManufacturerId), s); }
                if (tpm.ManufacturerVersion) { x += addDetailItem("Version", EscapeHtml(tpm.ManufacturerVersion), s); }
                if (tpm.IsActivated != null) { x += addDetailItem("Aktiviert", (tpm.IsActivated ? "Ja" : "No"), s); }
                if (tpm.IsEnabled != null) { x += addDetailItem("aktiviert", (tpm.IsEnabled ? "Ja" : "No"), s); }
                if (tpm.IsOwned != null) { x += addDetailItem("Im Besitz", (tpm.IsOwned ? "Ja" : "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 : "Unbekannt") + '</b></div>';
                    if (battery.CycleCount) { x += addDetailItem("Zyklusanzahl", EscapeHtml(battery.CycleCount), s); }
                    if (battery.FullChargedCapacity) { x += addDetailItem("Volle geladene Kapazität", format("{0} mWh", battery.FullChargedCapacity), s); }
                    if (battery.EstimatedRuntime) { x += addDetailItem("Geschätzte Laufzeit", format("{0} Minuten", Math.floor((battery.EstimatedRuntime / 60))), s); }
                    if (battery.Chemistry) { x += addDetailItem("Chemie", EscapeHtml(battery.Chemistry), s); }
                    if (battery.DesignedCapacity) { x += addDetailItem("Designkapazität", format("{0} mWh", battery.DesignedCapacity), s); }
                    if (battery.ManufactureDate) { x += addDetailItem("Herstellungsdatum", EscapeHtml(battery.ManufactureDate), s); }
                    if (battery.ManufactureName) { x += addDetailItem("Herstellername", EscapeHtml(battery.ManufactureName), s); }
                    if (battery.SerialNumber) { x += addDetailItem("Seriennummer", EscapeHtml(battery.SerialNumber), s); }
                    if (battery.ChargeRate) { x += addDetailItem("Gebührensatz", format("{0} mW", battery.ChargeRate), s); }
                    if (battery.Charging != null) { x += addDetailItem("Aufladen", (battery.Charging ? "Ja" : "No"), s); }
                    if (battery.DischargeRate) { x += addDetailItem("Entladungsrate", format("{0} mW", battery.DischargeRate), s); }
                    if (battery.Discharging != null) { x += addDetailItem("Entladen", (battery.Discharging ? "Ja" : "No"), s); }
                    if (battery.RemainingCapacity) { x += addDetailItem("Verbleibende Kapazität", format("{0} mWh", battery.RemainingCapacity), s); }
                    if (battery.Voltage) { x += addDetailItem("Stromspannung", format("{0} V", (battery.Voltage / 1000)), s); }
                    if (battery.Health) { x += addDetailItem("Gesundheit", format("{0} %", battery.Health), s); }
                    if (battery.BatteryCharge) { x += addDetailItem("Batterieladung", format("{0} %", battery.BatteryCharge), s); }
                    x += '</div>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "Batterie", 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("Kapazität / Geschwindigkeit", EscapeHtml(format("{0} MB, {1} MHz", (m.Capacity / 1024 / 1024), m.Speed)), s); }
                        else if (m.Capacity) { x += addDetailItem("Kapazität", EscapeHtml(format("{0} MB", (m.Capacity / 1024 / 1024))), s); }
                        if (m.PartNumber) { x += addDetailItem("Teilenummer", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

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

                    if (x != '') { sections.push({ name: "Speicher", 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("Kapazität / Geschwindigkeit", format("{0}, {1}", m.Size, m.Speed), s); }
                        else if (m.Size) { x += addDetailItem("Kapazität", format("{0}", (m.Size)), s); }
                        if (m.PartNumber) { x += addDetailItem("Teilenummer", EscapeHtml((m.Manufacturer && m.Manufacturer != '') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';
                    if (x != '') { sections.push({ name: "Speicher", 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("Modell", 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("Kapazität", format("{0} MB", Math.floor(m.Size / 1024 / 1024)), s); }
                            if (typeof m.Size == 'string') { x += addDetailItem("Kapazität", 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("Status", EscapeHtml(foundObject.Status), s);
                        }
                        x += '</div>';
                    }
                }
                x += '</table>';

                if (x != '') { sections.push({ name: "Speicher", 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: "Unbekannt", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" };
                const conversionStatus = { 1: "Vollständig verschlüsselt", 2: "Verschlüsselung wird durchgeführt", 3: "Decryption In Progress", 4: "Encryption Paused", 5: "Decryption Paused" };
                for (var i in hardware.windows.volumes) {
                    var m = hardware.windows.volumes[i];
                    x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                    x += '<div style=margin-bottom:3px><b>' + i + ':' + (((m.name == null) || (m.name == '')) ? '' : (' - ' + EscapeHtml(m.name))) + '</b></div>';
                    if (m.size) {
                        var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                        var j = parseInt(Math.floor(Math.log(Math.abs(m.size)) / Math.log(1024)), 10);
                        var fsize = (j === 0 ? `${m.size} ${sizes[j]}` : `${(m.size / (1024 ** j)).toFixed(2)} ${sizes[j]}`);
                        x += addDetailItem("Kapazität", 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("Verbleibende Kapazität", 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] : "Unbekannt"; }
                        x += addDetailItem("Dateisystem", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Unbekannt" : 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 = "Gesperrt";
                            } else {
                                bitlockerState = (conversionStatus.hasOwnProperty(m.volumeStatus) ? conversionStatus[m.volumeStatus] : "Unbekannt");
                            }
                        }
                        bitlockerState += (encMethod.hasOwnProperty(m.encryptionMethod) ? ' / ' + encMethod[m.encryptionMethod] : ' / ' + "Unbekannt");
                        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: "Logische Laufwerke", 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("Kapazität", 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("Verbleibende Kapazität", EscapeHtml(fsize), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("Dateisystem", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Unbekannt" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Logische Laufwerke", 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("Kapazität", EscapeHtml(m.size), s);
                    }
                    if (m.available) {
                        x += addDetailItem("Verbleibende Kapazität", EscapeHtml(m.available), s);
                    }
                    if (m.type) {
                        var type = (m.removable == true ? "Removable" : (m.cdrom == true ? "CD-ROM" : ''));
                        x += addDetailItem("Dateisystem", (type != '' ? (type + ' / ') : '') + (m.type == 'Unknown' ? "Unbekannt" : EscapeHtml(m.type)), s);
                    }
                    x += '</div>';
                }
                if (x != '') { sections.push({ name: "Logische Laufwerke", 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', "Keine Informationen zu diesem Gerät.");
            } else {
                QH('p17info2', x);
            }
        }

        function deviceDetailsShowBitlockerInfo(drive, identifier, password) {
            if (xxdialogMode) return false;
            var x = '<div><p>' + "Kennung" + '</p><p style=user-select:text;font-weight:bold>' + (identifier ? decodeURIComponent(identifier) : "Unbekannt") + '</p>';
            x += '<p>' + "Recovery Password" + '</p><p style=user-select:text;font-weight:bold>' + (password ? decodeURIComponent(password) : "Unbekannt") + '</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>' + "Desktop-Apps werden geladen..." + '</span>');
            QH('p18html', '<div style="text-align:center;padding:50px;color:gray">' + "Installierte Software wird geladen..." + '</div>');
            if ((currentNode.conn & 1) == 0) {
                QH('p18html', '<div style="text-align:center;padding:50px;color:#cc0000"><b>' + "Gerät ist offline." + '</b></div>');
                QH('p18Status', '<span>' + "Offline" + '</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>' + "FEHLER:" + e.message + '</span>');
                installedAppsLoading = false;
                installedAppsLoadingType = 0;
            }
        }

        function loadStoreApps() {
            if (currentNode == null) return;
            if (installedAppsLoading) return;
            installedAppsLoading = true;
            installedAppsLoadingType = 2;
            QH('p18Status', '<span>' + "Store-Apps werden geladen..." + '</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> ' + "Desktop-Anwendungen" + ' (' + 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">' + "Name" + '</th>';
            x += '<th style="text-align:left;width:120px">' + "Version" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Herausgeber" + '</th>';
            x += '<th style="text-align:left;width:100px">' + "Installationsdatum" + '</th>';
            x += '<th style="text-align:left;width:200px">' + "Standort" + '</th>';
            x += '<th style="text-align:center;width:100px">' + "Aktionen" + '</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 ? "Keine passenden Anwendungen" + '"' + EscapeHtml(searchTerm) + '"' : "Keine Desktop-Anwendungen gefunden";
                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-Apps" + ' (' + 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">' + "Name" + '</th>';
				x += '<th style="text-align:left;width:120px">' + "Version" + '</th>';
				x += '<th style="text-align:left;width:250px">' + "Paket" + '</th>';
				x += '<th style="text-align:center;width:100px">' + "Umfang" + '</th>';
				x += '<th style="text-align:center;width:80px">' + "Aktionen" + '</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 ? "Keine passenden Store-Apps" + '"' + EscapeHtml(searchTerm) + '"' : "Keine Store-Apps gefunden";
					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="' + "Nur für aktuellen Benutzer installiert" + '">' + "Benutzer" + '</span>';
							} else if (app.scope.indexOf('System') >= 0) {
								scopeBadge = '<span class="badge bg-info" title="' + "Für alle Benutzer installiert" + '">' + "System" + '</span>';
							}
							if (app.scope.indexOf('Prov') >= 0) {
								scopeBadge += ' <span class="badge bg-secondary" title="' + "Für neue Benutzer bereitgestellt" + '">' + "Ber" + '</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 = "Desktop:" + ' ' + shownDesktop + '/' + totalDesktop;
            if (showStoreApps) {
                statusText += ' | ' + "Store:" + ' ' + 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>' + "FEHLER:" + "Kein Deinstallationsbefehl verfügbar" + '</span>');
                return;
            }
            var dialogContent = '<div style="max-width:500px">';
            dialogContent += '<p><b>' + "Diese Software deinstallieren?" + '</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>' + "Eine stille Deinstallation wird durchgeführt" + '</i></p>';
            dialogContent += '</div>';
            setModalContent('xxAddAgent', "Software deinstallieren", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
                QH('p18Status', '<span>' + "Deinstallieren..." + '</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', "Deinstallation gestartet. Klicken Sie auf Aktualisieren"); }, 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>' + "Diese Store-App entfernen?" + '</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', "Store-App entfernen", dialogContent);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function() {
				QH('p18Status', '<span>' + "Entfernen..." + '</span>');
				meshserver.send({ action: 'software', nodeid: currentNode._id, type: 'uninstallstoreapp', value: '"' + cmd.replace(/"/g, '\\"') + '"' });
				setTimeout(function() { QH('p18Status', "Entfernungsbefehl gesendet. Klicken Sie auf Aktualisieren"); }, 2000);
			});
		}

        function handleUninstallResponse(data) {
            try {
                var result = JSON.parse(data);
                if (result.success) {
                    QH('p18Status', '<span>✓ ' + "Erfolg" + '</span> ' + "Klicken Sie auf Aktualisieren");
                } else if (result.error) {
                    QH('p18Status', '<span>✗ ' + "FEHLER:" + 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', ' - ' + "Meine Server-Konsole");
                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) ? "Agent ist online" : "Agent ist offline"
                    if ((consoleNode.conn & 16) != 0) { onlineText += ", MQTT ist verbunden" }
                    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', "Zugriff verweigert");
                    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>' + "DRÜCKEN" + '&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>' + "Vorgabe-Serverkern hochladen" + '</option>' +
                    '<option value=2>' + "Den Kern löschen" + '</option>' +
                    '<option value=3>' + "Kern-Datei hochladen" + '</option>' +
                    '<option value=4>' + "Agent sanft trennen" + '</option>' +
                    '<option value=5>' + "Agent hart trennen" + '</option>' +
                    '<option value=6>' + "Wiederherstellungkern hochladen" + '</option>' +
                    '<option value=7>' + "Kleinstkern hochladen" + '</option>' +
                    '<option value=8>' + "Agent/Dienst neustarten" + '</option>' +
                    '<option value=9>' + "Aktualisierung des Agents erzwingen" + '</option></select>';
                setModalContent('xxAddAgent', "Agent-Aktion durchführen", addHtmlFormFloating("Aktion", htmlValue));
                showModal('xxAddAgentModal', 'idx_dlgOkButton', p15uploadCoreEx);
            }
        }

        function p15uploadCoreEx() {
            if (Q('d3coreMode').value == 1) {
                // Upload default core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'default' });
            } else if (Q('d3coreMode').value == 2) {
                // Clear the core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'clear' });
            } else if (Q('d3coreMode').value == 3) {
                // Upload file as core
                p15uploadCore2();
            } else if (Q('d3coreMode').value == 4) {
                // Soft disconnect the mesh agent
                meshserver.send({ action: 'agentdisconnect', nodeid: consoleNode._id, disconnectMode: 1 });
            } else if (Q('d3coreMode').value == 5) {
                // Hard disconnect the mesh agent
                meshserver.send({ action: 'agentdisconnect', nodeid: consoleNode._id, disconnectMode: 2 });
            } else if (Q('d3coreMode').value == 6) {
                // Upload a recovery core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'recovery' });
            } else if (Q('d3coreMode').value == 7) {
                // Upload a tiny core
                meshserver.send({ action: 'uploadagentcore', nodeids: [consoleNode._id], type: 'tiny' });
            } else if (Q('d3coreMode').value == 8) {
                // Restart MeshAgent service
                meshserver.send({ action: 'msg', type: 'console', nodeid: consoleNode._id, value:'service restart' });
            } else if (Q('d3coreMode').value == 9) {
                // Update mesh agent
                meshserver.send({ action: 'updateAgents', nodeids: [consoleNode._id] });
            }
        }

        // Called then user opts to upload a file as core
        function p15uploadCore2() {
            if (xxdialogMode) return;
            Q('d3localmodeform').action = 'uploadmeshcorefile.ashx';
            Q('d3auth').value = authCookie;
            Q('d3filter').value = '.js';
            Q('d3attrib').value = currentNode._id;
            setModalContent('xxAddAgent', "Mesh-Agent-Kern hochladen", '');
            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', "Bisherige Logins", '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', "Konto-Bild verwalten", 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>' + "Verifizierte Telefonnummer" + '</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() />' + "Telefonnummer entfernen" + '</label></div>';
                setModalContent('xxAddAgent', "Telefonische Benachrichtigungen", 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>' + "Geben Sie Ihre SMS-fähige Telefonnummer ein. Nach der Überprüfung kann die Nummer für die Anmeldeüberprüfung und andere Benachrichtigungen verwendet werden.";
                x += '<br /><br /><div style=width:100%;text-align:center>' + "Telefonnummer:" + ' <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', "Telefonische Benachrichtigungen", 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>' + "Verifizierte Nutzerkennung" + '</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() />' + "Benachrichtigen entfernen" + '</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>' + "Geben Sie ihren Messengerdienst und Nutzernamen ein. Sobald Sie verifiziert sind, kann dieser Server Ihnen Loginverifikationen und andere Benachrichtigungen senden." + '<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>' + "Dienst" + '<td>' + y;
                x += '<tr><td>' + "Nutzername" + '<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">' + "Treten Sie dem Discordserver bei um Benachrichtigungen zu erhalten." + '</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">' + "Informationen bei 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'] = "Benutzername:0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "benutzername@server.de"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Nutzerschlüssel"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Thema"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "benutzername@beispiel.de"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Benutzername"; }
            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', "E-Mail-Authentifikation", "Wenn diese Option aktiviert ist, erhalten Sie bei jeder Anmeldung die Option, ein Anmeldetoken für zusätzliche Sicherheit an Ihr E-Mail-Konto zu senden." + '<br /><br /><label><input id=email2facheck type=checkbox class="form-check-input me-2" ' + (emailU2Fenabled ? 'checked' : '') + '/>' + "Aktivieren Sie die E-Mail-Zwei-Faktor-Authentifizierung." + '</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', "Authentifikator-Anwendung", ('<div id=d2optinfo>' + "Laden..." + '</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', "Authentifikator-Anwendung", "Bestätigen Sie das Entfernen der Zweifaktor-Anmeldung mittels Authentifikator-Applikation?");
            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', "Authentifizierungsgerät", "Entfernen des Push-Authentifizierungsgeräts bestätigen?");
                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', "Authentifizierungsgerät", '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 = "Wählen Sie ein Gerät aus, das für die Push-Benachrichtigungsauthentifizierung registriert werden soll. Nach der Auswahl fordert das Gerät zur Bestätigung auf." + '<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("Gerät", y);
                    setModalContent('xxAddAgent', "Authentifizierungsgerät", 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 = "Geben Sie den Namen des hinzuzufügenden Schlüssels ein." + '<br /><br />';
                x += addHtmlFormFloating("Schlüsselname", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "Mein Schlüssel" + '" onkeyup=account_addhkeyValidate(event,2) />');
            } else if (type == 2) {
                var x = "Geben Sie einen Schlüsselnamen ein, wählen Sie OTP und drücken Sie die Taste an Ihrem YubiKey&trade;." + '<br /><br />';
                x += addHtmlFormFloating("Schlüsselname", '<input id=dp1keyname class="form-control" maxlength=20 autocomplete=off placeholder="' + "Mein Schlüssel" + '" 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', "Sicherheitsschlüssel hinzufügen", 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', "Sicherheitsschlüssel hinzufügen", '<br />' + "Überprüfen..." + '<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': "Albanian", 'ar': "Arabisch (Standard)", 'ar-dz': "Arabisch (Algerien)", 'ar-bh': "Arabisch (Bahrain)", 'ar-eg': "Arabisch (Ägypten)", 'ar-iq': "Arabisch (Irak)", 'ar-jo': "Arabisch (Jordanien)", 'ar-kw': "Arabisch (Kuwait)", 'ar-lb': "Arabisch (Libanon)", 'ar-ly': "Arabisch (Libyen)", 'ar-ma': "Arabisch (Marokko)", 'ar-om': "Arabisch (Oman)", 'ar-qa': "Arabisch (Katar)", 'ar-sa': "Arabisch (Saudi-Arabien)", 'ar-sy': "Arabisch (Syrien)", 'ar-tn': "Arabisch (Tunesien)", 'ar-ae': "Arabisch (V.A.E.)", 'ar-ye': "Arabisch (Jemen)", 'an': "Aragonesisch", 'hy': "Armenisch", 'as': "Assamesisch", 'ast': "Asturisch", 'az': "Aserbaidschanisch", 'eu': "Baskisch", 'bg': "Bulgarisch", 'be': "Belarussisch", 'bn': "Bengali", 'bs': "Bosnisch", 'br': "Bretonisch", 'my': "Birmanisch", 'ca': "Katalanisch", 'ch': "Chamorro", 'ce': "Tschetschenisch", 'zh': "Chinesisch", 'zh-hk': "Chinesisch (Hong Kong)", 'zh-cn': "Chinesisch (VR China)", 'zh-sg': "Chinesisch (Singapur)", 'zh-tw': "Chinesisch (Taiwan)", 'cv': "Tschuwaschisch", 'co': "Korsisch", 'cr': "Cree", 'hr': "Kroatisch", 'cs': "Tschechisch", 'da': "Dänisch", 'nl': "Niederländisch (Standard)", 'nl-be': "Niederländisch (Belgisch)", 'en': "Englisch", 'en-au': "Englisch (Australien)", 'en-bz': "Englisch (Belize)", 'en-ca': "Englisch (Kanada)", 'en-ie': "Englisch (Irland)", 'en-jm': "Englisch (Jamaika)", 'en-nz': "Englisch (Neuseeland)", 'en-ph': "Englisch (Philippinen)", 'en-za': "Englisch (Südafrika)", 'en-tt': "Englisch (Trinidad Tobago)", 'en-gb': "Englisch (Vereinigtes Königreich)", 'en-us': "Englisch (USA)", 'en-zw': "Englisch (Simbabwe)", 'eo': "Esperanto", 'et': "Estnisch", 'fo': "Färöisch", 'fa': "Persisch", 'fj': "Fidschianisch", 'fi': "Finnisch", 'fr': "Französisch (Standard)", 'fr-be': "Französisch (Belgien)", 'fr-ca': "Französisch (Kanada)", 'fr-fr': "Französisch (Frankreich)", 'fr-lu': "Französisch (Luxemburg)", 'fr-mc': "Französisch (Monaco)", 'fr-ch': "Französisch (Schweiz)", 'fy': "Friesisch", 'fur': "Friaulisch", 'gd': "Gälisch (Schottisch)", 'gd-ie': "Gälisch (Irisch)", 'gl': "Galicisch", 'ka': "georgisch", 'de': "Deutsch (Standard)", 'de-at': "Deutsch (Österreich)", 'de-de': "Deutsch (Deutschland)", 'de-li': "Deutsch (Liechtenstein)", 'de-lu': "Deutsch (Luxemburg)", 'de-ch': "Deutsch (Schweiz)", 'el': "Griechisch", 'gu': "Gujurati", 'ht': "Haitianisch", 'he': "Hebräisch", 'hi': "Hindi", 'hu': "Ungarisch", 'is': "Isländisch", 'id': "Indonesisch", 'iu': "Inuktitut", 'ga': "Irisch", 'it': "Italienisch (Standard)", 'it-ch': "Italienisch (Schweiz)", 'ja': "Japanisch", 'kn': "Kannada", 'ks': "Kashmiri", 'kk': "Kasachisch", 'km': "Khmer", 'ky': "Kirgisisch", 'tlh': "Klingonisch", 'ko': "Koreanisch", 'ko-kp': "Koreanisch (Nordkorea)", 'ko-kr': "Koreanisch (Südkorea)", 'la': "Latein", 'lv': "Lettisch", 'lt': "Litauisch", 'lb': "Luxemburgisch", 'mk': "Mazedonisch (EJRM)", 'ms': "Malaiisch", 'ml': "Malayalam", 'mt': "Maltesisch", 'mi': "Maori", 'mr': "Marathi", 'mo': "Moldawisch", 'nv': "Navajo", 'ng': "Ndonga", 'ne': "Nepali", 'no': "Norwegisch", 'nb': "Norwegisch (Bokmal)", 'nn': "Norwegisch (Nynorsk)", 'oc': "Okzitanisch", 'or': "Oriya", 'om': "Oromo", 'fa-ir': "Persisch (Iran)", 'pl': "Polnisch", 'pt': "Portugiesisch", 'pt-br': "Portugiesisch (Brasilien)", 'pa': "Punjabi", 'pa-in': "Punjabi (Indien)", 'pa-pk': "Punjabi (Pakistan)", 'qu': "Quechua", 'rm': "Rätoromanisch", 'ro': "Rumänisch", 'ro-mo': "Rumänisch (Moldawien)", 'ru': "Russisch", 'ru-mo': "Russisch (Moldawien)", 'sz': "Sami (Lappisch)", 'sg': "Sango", 'sa': "Sanskrit", 'sc': "Sardisch", 'sd': "Sindhi", 'si': "Singhalesisch", 'sr': "Serbisch", 'sk': "Slowakisch", 'sl': "Slowenisch", 'so': "Somani", 'sb': "Sorbisch", 'es': "Spanisch", 'es-ar': "Spanisch (Argentinien)", 'es-bo': "Spanisch (Bolivien)", 'es-cl': "Spanisch (Chile)", 'es-co': "Spanisch (Kolumbien)", 'es-cr': "Spanisch (Costa Rica)", 'es-do': "Spanisch (Dominikanische Republik)", 'es-ec': "Spanisch (Ecuador)", 'es-sv': "Spanisch (El Salvador)", 'es-gt': "Spanisch (Guatemala)", 'es-hn': "Spanisch (Honduras)", 'es-mx': "Spanisch (Mexiko)", 'es-ni': "Spanisch (Nicaragua)", 'es-pa': "Spanisch (Panama)", 'es-py': "Spanisch (Paraguay)", 'es-pe': "Spanisch (Peru)", 'es-pr': "Spanisch (Puerto Rico)", 'es-es': "Spanisch (Spanien)", 'es-uy': "Spanisch (Uruguay)", 'es-ve': "Spanisch (Venezuela)", 'sx': "Sutu", 'sw': "Suaheli", 'sv': "Schwedisch", 'sv-fi': "Schwedisch (Finnland)", 'sv-sv': "Schwedisch (Schweden)", 'ta': "Tamil", 'tt': "Tatar", 'te': "Teluga", 'th': "Thai", 'tig': "Tigre", 'ts': "Tsonga", 'tn': "Tswana", 'tr': "Türkisch", 'tk': "Turkmenisch", 'uk': "Ukrainisch", 'hsb': "Obersorbisch", 'ur': "Urdu", 've': "Venda", 'vi': "Vietnamesisch", 'vo': "Volapuk", 'wa': "Wallonisch", 'cy': "Walisisch", 'xh': "Xhosa", 'ji': "Jiddisch", 'zu': "Zulu" };
        var loclistex = { 'zh-chs': "Vereinfachtes Chinesisch", 'zh-cht': "Chinesische Tradition)" };
        function account_showLocalizationSettings() {
            if (xxdialogMode) return false;
            var n = getstore('loctag', 0), y = '';
            var x = '<select id=d2locselect class="form-select"><option value="*">' + "Browsereinstellung des Benutzers" + '</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 += "Das Ändern der Sprache erfordert eine Aktualisierung der Seite." + '<br /><br />';
                var z = '<select id=d2langselect class="form-select"><option value="*">' + "Browsereinstellung des Benutzers" + '</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("Sprache", z);
            }
            y += addHtmlFormFloating("Datum & Uhrzeit", x);

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

            setModalContent('xxAddAgent', "Lokalisierungseinstellungen", 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 = "Erstellen Sie einen temporären Benutzernamen und ein Passwort, die als alternative Anmeldung zu Ihrem Konto verwendet werden können. Dies ist nützlich, um Tools oder anderen Diensten den Zugriff auf Ihr Konto zu ermöglichen." + '<br /><br />';
            var options = { 0: "Unbegrenzt", 1: "1 Minute", 5: "5 Minuten", 10: "10 Minuten", 15: "15 Minuten", 30: "30 Minuten", 45: "45 Minuten", 60: "60 Minuten", 120: "2 Stunden", 240: "4 Stunden", 480: "8 Stunden", 720: "12 Stunden", 960: "16 Stunden", 1440: "24 Stunden", 2880: "2 Tage", 5760: "4 Tage", 10080: "7 Tage" }
            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="' + "Token-Name" + '" onchange=account_createLoginTokenValidate() onkeyup=account_createLoginTokenValidate()>';
            x += '<label for=d2tokenName>' + "Token-Name" + '</label>';
            x += '</div>';
            x += addHtmlFormFloating("Ablaufzeit", '<select class="boxsize form-select" id=d2tokenExpire>' + y + '</select>')

            setModalContent('xxAddAgent', "Login-Token erstellen", 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 />' + "Benachrichtigungston" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyGroupName class="form-check-input me-2" type=checkbox />' + "Gerätegruppenname anzeigen" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Geräteverbindungen" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Gerätetrennungen" + '</label></div>';
            x += '<div class="form-check"><label><input id=p2notifyIntelAmtKvmActions class="form-check-input me-2" type=checkbox />' + "Intel&reg; AMT Desktop- und Seriell-Ereignisse" + '</label></div>';

            setModalContent('xxAddAgent', "Benachrichtungseinstellungen", 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 = "Auf OK klicken, um eine Bestätigungsmail an folgende Adresse zu senden:" + '<br /><div style=padding:8px><b>' + EscapeHtml(userinfo.email) + '</b></div>' + "Bitte warten Sie einige Minuten, um die Bestätigung zu erhalten.";
            setModalContent('xxAddAgent', "E-Mail-Verifizierung", 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 = "Ändern Sie hier Ihre Konto-E-Mail-Adresse." + '<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', "E-Mail-Adresse ändern", 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 = "Um dieses Konto zu löschen, geben Sie das Konto-Passwort in beide untenstehenden Eingabefelder ein und wählen Sie 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', "Konto löschen", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                dialogclose(1);
            });

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

        function account_showChangePassword() {
            if (xxdialogMode) return false;
            var x = "Ändern Sie Ihr Kontopasswort, indem Sie das alte und das neue Passwort zweimal in die Felder unten eingeben.";
            if (features & 0x00010000) { " Passwort-Hinweis kann verwendet werden, wird jedoch nicht empfohlen."; }
            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>' + "Voraussetzungen: " + 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', "Passwort ändern", 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: "Standard" },
                { 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">' + "Wählen Sie ein Theme" + ':</label>';
            x += '<select id="theme-switcher" class="form-select" onchange="account_switchThemeEx()">';
            // Add "Recent Themes" optgroup
            if (lastThemes.length > 0) {
                x += '<optgroup label="' + "Neueste Themes" + '"">';
                lastThemes.forEach(function (theme) {
                    var selected = theme === currentTheme ? ' selected' : '';
                    x += `<option value="${theme}"${selected}>${theme.charAt(0).toUpperCase() + theme.slice(1)}</option>`;
                });
                x += '</optgroup>';
            }
            // Add "All Themes" optgroup, excluding duplicates
            x += '<optgroup label="' + "Alle Themes" + '">';
            themes.forEach(function (theme) {
                if (!recentThemesSet.has(theme.value)) {
                    var selected = theme.value === currentTheme ? ' selected' : '';
                    x += `<option value="${theme.value}"${selected}>${theme.label}</option>`;
                }
            });
            x += '</optgroup>';
            x += '</select></div>';

            setModalContent('xxAddAgent', "Theme ändern", 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', "Neue Gerätegruppe", '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', "Konto-Sicherheit", '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', "Konto-Sicherheit", '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 = "Eine neue Gerätegruppe mit den folgenden Optionen erstellen." + '<br /><br />';
            var localGroupType = '';

            x += addHtmlFormFloating("Name", '<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>' + "Lokale Geräte, kein Agent" + '</option>'; }
            if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=103>' + "Keine Agentengeräte werden über den Agenten weitergeleitet" + '</option>'; }
            if (features2 & 0x10000) {
                if ((features & 1) == 0) { localGroupType += '<option value=4>' + "IP-KVM / Power-Gerät" + '</option>'; }
                if (((features & 2) == 0) && (relayDevices.length > 0)) { localGroupType += '<option value=104>' + "IP-KVM / Stromversorgungsgerät, das über einen Agenten weitergeleitet wird" + '</option>'; }
            }
            x += addHtmlFormFloating("Modus", '<select id=dp2meshtype class=form-select onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=2>' + "Mittels Software-Agent verwalten" + '</option><option value=1>' + "Nur Intel&reg; AMT, kein Agent" + '</option>' + localGroupType + '</select>');
            if (relayDevices.length > 0) {
                x += '<div id=d2devrelaydiv style=display:none>';
                relayDevices.sort(nameSort);
                var relayDevices2 = [];
                for (var i in relayDevices) { relayDevices2.push('<option value="' + relayDevices[i]._id + '">' + EscapeHtml(relayDevices[i].name) + ' (' + EscapeHtml(relayDevices[i].meshnamel) + ')' + '</option>'); }
                x += addHtmlFormFloating("Relay-Gerät", '<select id=d2devrelay onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select >' + relayDevices2.join('') + '</select>');
                x += '</div>';
            }
            x += addHtmlFormFloating("Beschreibung", '<textarea id=dp2meshdesc maxlength=1024 class=form-control></textarea>');
            x += '<div id=d2ipkvm style=display:none><hr />';
            x += addHtmlFormFloating("Modell", '<select id=dp2ipkvmmodel onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) class=form-select ><option value=1>' + "Raritan-Dominion KX III" + '</option><option value=2>' + "Web Power Switch 7" + '</option></select>');
            x += addHtmlFormFloating("Hostname", '<input id=dp2ipkvmhost maxlength=128 placeholder="Hostname" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Benutzername", '<input id=dp2ipkvmuser maxlength=128 placeholder="Username" onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) class=form-control />');
            x += addHtmlFormFloating("Passwort", '<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', "Neue Gerätegruppe", 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 == "Eingabe") && (Q('dp2meshname').value.length > 0)) { Q('dp2meshtype').focus(); }
            if ((x == 2) && (e != null) && (e.key == "Eingabe")) { 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>' + "Stark" + '<span>'; } else if (passStrength >= 60) { r = '<span style=color:blue>' + "Gut" + '<span>'; } else { r = '<span style=color:red>' + "Schwach" + '<span>'; }
                } else {
                    // Password requirements provided, use that
                    var passReq = checkPasswordRequirements(Q('apassword1').value, passRequirements);
                    if (passReq == false) { ok = false; r = '<span style=color:red>' + "Richtlinie" + '<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 = "Partielle Rechte";
                if (meshrights == 0xFFFFFFFF) rights = "Gesamtadministrator"; else if (meshrights == 0) rights = "Keine Berechtigungen";

                // 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>' + "Aktive Login-Token" + '</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> ' + "Neu" + '</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>' + "Name" + '</th><th scope=col style=text-align:left>' + "Benutzername" + '</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="' + "Anmeldetoken entfernen" + '" role=button><i class="fa-solid fa-trash text-danger fa-xs"></i></a>';
                    var details = '';
                    if (ltoken.expire != 0) { details = EscapeHtml(format("Läuft ab {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 = "Entfernen dieses Anmeldetokens bestätigen?" + '<br /><br />';
            x += addHtmlValue("Name", EscapeHtml(token.name));
            x += addHtmlValue("Benutzername", EscapeHtml(token.tokenUser));
            setModalContent('xxAddAgent', "Anmeldetoken entfernen", 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>' + "Richten Sie diesen Server so ein, dass Backups automatisch auf Google Drive hochgeladen werden. Erstellen und geben Sie zunächst eine Google Drive ClientID und ClientSecret für Ihr Konto ein." + '</div><br />';
                x += addHtmlValue("Anmeldedaten", '<a href="https://console.developers.google.com/apis/api/drive.googleapis.com/credentials" rel="noreferrer noopener" target="_blank">' + "Google Drive Console" + '</a>');
                x += addHtmlFormFloating("Kunden ID", '<input id=gdclientid class="form-control" placeholder="xxxxxxxx.apps.googleusercontent.com" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                x += addHtmlFormFloating("Kundengeheimnis", '<input id=gdclientsecret class="form-control" onkeyup=server_setupGoogleDriveBackupCheck1()></input>');
                setModalContent('xxAddAgent', "Google Drive Backup", 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>' + "Das Google Drive-Backup ist derzeit aktiv." + '</div>';
                x += '<br /><label><input id=gdcheck type=checkbox class="form-check-input me-2" onchange=server_setupGoogleDriveBackupCheck2() />' + "Konfiguration entfernen" + '</label>';
                setModalContent('xxAddAgent', "Google Drive Backup", 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 = "Optionales Kennwort für die Sicherung eingeben" + '<br /><br />';
            x += '<tr><td align=right>' + nobreak("Kennwort: ") + '</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') + ' /> ' + "Systemkennwort verwenden" + '</label></div>';
            setModalContent('xxAddAgent', "Server-Datensicherung herunterladen", 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 = "Server unter Verwendung einer Sicherungskopie wiederherstellen," + ' <span style=color:red>' + "Dies wird die existierenden Server-Daten löschen." + '</span> ' + "Fahren Sie nur fort, wenn Sie wissen, was Sie tun." + '<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("Sicherungskennwort: ") + '</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') + ' /> ' + "Systemkennwort verwenden" + '</label></div>';
            setModalContent('xxAddAgent', "Server wiederherstellen", 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', "Server wiederherstellen", "Sicherung wird hochgeladen");
            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', "Server wiederherstellen", "Netzwerkfehler: " + response.status);
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        QE('idx_dlgOkButton', true);
                    }
                })
                .catch((error) => {
                    setModalContent('xxAddAgent', "Server wiederherstellen", "Fehler: " + 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-Version", "Laden...");
            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 != "Unbekannt") && (xxdialogTag.current != xxdialogTag.stable) && (latestCheck == false)));
            QE('d2updateCheck2', ((xxdialogTag.latest != "Unbekannt") && (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', "Serverfehler", "Laden...");
            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>' + "Kein(e)" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span role=button tabindex=0 title="' + "Klicken Sie hier, um den Namen der Gerätegruppe zu bearbeiten" + '" 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("Unbekannt #{0}", currentMesh.mtype);
            if (currentMesh.mtype == 1) meshtype = "Nur Intel&reg; AMT, kein Agent";
            if (currentMesh.mtype == 2) meshtype = "Verwaltet mittels Software-Agent";
            if (currentMesh.mtype == 3) { if (currentMesh.relayid == null) { meshtype = "Lokale Geräte, kein Agent"; } else { meshtype = "Keine Agentengeräte werden über den Agenten weitergeleitet"; } }
            if (currentMesh.mtype == 4) { if (currentMesh.relayid == null) { meshtype = "IP-KVM-Gerät"; } else { meshtype = "IP-KVM-Gerät wird über Agent weitergeleitet"; } if (currentMesh.kvm.model == 1) { meshtype += ', ' + 'Raritan KX III'; } }

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

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

            // Display IP-KVM information if needed
            if (currentMesh.mtype == 4) {
                x += addHtmlValue("Hostname", currentMesh.kvm.host);
                x += addHtmlValue("Benutzername", currentMesh.kvm.user);
            }

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

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

            // Display features
            if (currentMesh.mtype != 3) {
                var meshFeatures = [];
                if (currentMesh.flags) {
                    if (currentMesh.flags & 1) { meshFeatures.push("Auto-Entfernen"); }
                    if (currentMesh.flags & 2) { meshFeatures.push((currentMesh.mtype == 4) ? "Synchronisierung des Portnamens" : "Hostname synchronisieren"); }
                    if (currentMesh.flags & 8) { meshFeatures.push("--agentname bevorzugen"); }
                    if (currentMesh.flags & 16) { meshFeatures.push("Überschreiben erlauben"); }
                    if ((serverinfo.devGroupSessionRecording == 1) && (currentMesh.flags & 4)) { meshFeatures.push("Sitzungen aufzeichnen"); }
                }
                if ((typeof currentMesh.expireDevs == 'number') && (currentMesh.expireDevs > 0)) { meshFeatures.push("Inaktive entfernen"); }
                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Kein(e)" + '</i>'; }
                x += addHtmlValue("Eigenschaften", 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("Desktop-Einverständnis & Datenschutzleiste"); } else if (consent & 0x0040) { meshFeatures.push("Desktop-Werkzeugleiste"); } else if (consent & 0x0008) { meshFeatures.push("Desktop-Einverständnis"); } else { if (consent & 0x0001) { meshFeatures.push("Desktop-Benachrichtigung"); } }
                if (consent & 0x0010) { meshFeatures.push("Terminal-Einverständnis"); } else { if (consent & 0x0002) { meshFeatures.push("Terminal-Benachrichtigung"); } }
                if (consent & 0x0020) { meshFeatures.push("Dateien-Einverständnis"); } else { if (consent & 0x0004) { meshFeatures.push("Dateien-Benachrichtigung"); } }
                if (consent == 7) { meshFeatures = ["Immer benachrichtigen"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Immer nachfragen"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Kein(e)" + '</i>'; }
                x += addHtmlValue("Benutzereinwilligung", 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("Verbinden"); }
                    if (meshNotify & 4) { meshNotifyStr.push("Trennen"); }
                    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("E-Mail ({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("Benachrichtigung ({0})", xx)); }
                    }
                    if (meshNotifyStr.length == 0) { meshNotifyStr.push('<i>' + "Kein(e)" + '</i>'); }
                    x += addHtmlValue("Benachrichtigungen", addLink(meshNotifyStr.join(', '), 'p20editMeshNotify()'));
                }
            }

            // Display invitation codes
            if ((features & 0x01000000) && (currentMesh.mtype == 2)) {
                var inviteCodeStr = '<i>' + "Kein(e)" + '</i>', icodes = false;
                if (currentMesh.invite != null) { icodes = true; inviteCodeStr = currentMesh.invite.codes.join(', '); /* + ', ' + currentMesh.invite.flags;*/ }
                x += addHtmlValue("Codes einladen", 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 = "Keine Richtlinie";
                if (currentMesh.amt) {
                    if (currentMesh.amt.type == 1) { intelAmtPolicy = "Deaktivieren"; }
                    else if (currentMesh.amt.type == 2) {
                        intelAmtPolicy = "Einfacher Client Control Mode (CCM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 3) {
                        intelAmtPolicy = "Einfacher Admin Control Mode (ACM)";
                        if (currentMesh.amt.cirasetup == 2) { intelAmtPolicy += " + CIRA"; }
                    } else if (currentMesh.amt.type == 4) {
                        intelAmtPolicy = "Vollautomatisch";
                    }
                }
                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=' + "Notizen" + ' title="' + "Zeige Notizen zu dieser Gerätegruppe" + '" 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> ' + "Benutzer hinzufügen" + '</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> ' + "Benutzergruppe hinzufügen" + '</button>'; }
                }
            }

            //if (meshrights & 4) { }
            if (currentMesh.mtype == 1) {
                if (meshrights & 1) { x += '<button class="btn btn-primary btn-sm me-2 mb-1" title="' + "Intel&reg; AMT Geräte importieren" + '" 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="' + "Führen Sie die Aktivierung und Konfiguration von Intel&reg; AMT durch." + '" onclick=\'return showAmtSetup("' + currentMesh._id + '")\'><i class="fa-solid fa-gear"></i> ' + "Konfiguration" + '</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="' + "Füge einen neuen Computer durch Installation des Mesh-Agenten zu dieser Gerätegruppe hinzu." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Agenten hinzufügen" + '</button>';
                x += '<button class="btn btn-primary btn-sm me-2 mb-1" onclick=\'return inviteAgentToMesh("' + currentMesh._id + '")\' title="' + "Laden Sie jemanden ein, den Mesh-Agent für diese Gerätegruppe zu installieren." + '"><i class="fa-solid fa-user-plus"></i> ' + "Einladen" + '</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="' + "Fügen Sie ein Gerät hinzu, das sich im lokalen Netzwerk befindet." + '"><i class="fa-solid fa-circle-plus"></i> ' + "Gerät hinzufügen" + '</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="' + "Schalten Sie Intel AMT in den Admin Control Mode (ACM)." + '" onclick=\'return showAmtAcmSetup()\'><i class="fa-solid fa-circle-arrow-down"></i> ' + "ACM" + '</button>';
            }

            x += '<table class="table table-hover table-striped" cellpadding=2 cellspacing=0><tbody><tr><th scope=col>' + "Benutzerberechtigungen" + '</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="' + "Entziehe Benutzerrechte an dieser Gerätegruppe" + '"><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="' + "Benutzer" + '" 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>' + "Aktive Gerätefreigabe" + '</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="' + "Link zur Gerätefreigabe" + '" 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="' + "Entfernen Sie die Gerätefreigabe" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
                    var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Dateien", "Terminal + Dateien", "Desktop + Dateien", "Desktop + Terminal + Dateien"][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} bis {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} vor {2} Minute", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        } else {
                            details = format("{0}, {1} vor {2} Minuten", type, printFlexDateTime(new Date(dshare.startTime)), dshare.duration);
                        }
                    }
                    if (dshare.recurring == 1) { details += ", täglich wiederkehrend"; }
                    if (dshare.recurring == 2) { details += ", wöchentlich wiederkehrend"; }
                    if (((dshare.p & 2) != 0) && (dshare.viewOnly === true)) { details += ", Nur Desktop anzeigen"; }
                    if (dshare.consent != null) {
                        if (dshare.consent == 0) { details += ", keine Zustimmung"; } else {
                            if ((dshare.consent & 0x0038) != 0) { details += "Aufforderung zur Zustimmung"; }
                            if ((dshare.consent & 0x0040) != 0) { details += ", Symbolleiste"; }
                        }
                    }
                    var guestName = EscapeHtml(dshare.guestName);
                    if (dshare.publicid.startsWith('AS:node/')) { guestName = '<i>' + "Agent-Selbstfreigabe" + '</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="' + "Geräteliste herunterladen" + '" src="images/link4.png" /></a>';
                if ((features & 1) == 0) { dllist += ' <a onclick=meshImportDeviceList()><img title="' + "Geräteliste importieren" + '" 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>' + "Geräte" + '</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>' + "Kein(e)" + '</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>' + "Gruppe löschen" + '</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 = "Importieren Sie eine Liste lokaler Intel&reg; AMT-Geräte im MeshCommander-JSON-Format." + '<br /><br /><input type=file id=d4importFile class="form-control" accept=".json" onchange=meshImportDeviceListValidate() />';
            setModalContent('xxAddAgent', "Importieren von Intel&reg; AMT-Geräten", 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', "Importieren von Intel&reg; AMT-Geräten", format("Ungültige JSON-Datei: {0}.", ex));
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                    return;
                }
                if ((typeof j == 'object') && (typeof j.webappversion == 'string') && (typeof j.computers == 'object') && (Array.isArray(j.computers))) {
                    var ok = 0;
                    for (var k in j.computers) {
                        var computer = j.computers[k];
                        if ((typeof computer.name == 'string') && (typeof computer.host == 'string') && (typeof computer.user == 'string') && (typeof computer.pass == 'string')) {
                            var cmd = { action: 'addamtdevice', meshid: currentMesh._id, devicename: computer.name, hostname: computer.host, amtusername: computer.user, amtpassword: computer.pass, amttls: (computer.tls == 1) ? 1 : 0 };
                            if (typeof computer.ver == 'string') { cmd.ver = computer.ver; }
                            if (typeof computer.pstate == 'number') { cmd.state = computer.pstate; }
                            if (typeof computer.digestrealm == 'string') { cmd.realm = computer.digestrealm; }
                            if (typeof computer.tlscerthash == 'string') { cmd.hash = computer.tlscerthash; }
                            meshserver.send(cmd);
                            ok++;
                        }
                    }
                    if (ok == 0) {
                        setModalContent('xxAddAgent', "Benutzerkonten-Import", 'Unable to import any device.');
                    }
                } else {
                    setModalContent('xxAddAgent', "Importieren von Intel&reg; AMT-Geräten", '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>' + "Einfacher Admin Control Mode (ACM)" + '</option>'; }
            x += addHtmlFormFloating("Modus", '<select id=dp20amtpolicy class="form-select" onchange=p20editMeshAmtChange()><option value=0>' + "Keine Richtlinie" + '</option><option value=1>' + "Deaktivieren" + '</option><option value=2>' + "Einfacher Client Control Mode (CCM)" + '</option>' + acmoption + '<option value=4>' + "Vollautomatisch" + '</option></select>')
            x += '<div id=dp20amtpolicydiv></div>';

            setModalContent('xxAddAgent', "Intel&reg; AMT Richtlinie", 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>' + "Behalten Sie das vorhandene Passwort bei" + '</option>' : '';
                x += addHtmlFormFloating("Passwort", '<select id=dp20amtpass class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Passwort zufällig auswählen" + '</option>' + keeppass + '<option value=2>' + "Wählen Sie ein neues Passwort" + '</option></select>')
                x += '<div id=dp20amtpassdiv style=display:none>';
                x += addHtmlFormFloating("Neues Passwort*", '<input id=dp20amtpolicypass type=password class="form-control" maxlength=32 placeholder="New password*" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy() autocomplete=off />');
                x += addHtmlFormFloating("Neues Passwort*", '<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-Modus", '<select id=dp20amtccmmode class="form-select" onchange=dp20amtValidatePolicy() onkeyup=dp20amtValidatePolicy()><option value=0>' + "Nicht ändern, CCM bei Einrichtung beibehalten" + '</option><option value=1>' + "Deaktivieren Sie CCM, wenn Sie eingerichtet sind" + '</option><option value=2>' + "Aktivieren Sie CCM, wenn ACM fehlschlägt" + '</option></select>'); }
                x += '<div id=dp20amtbadpassdiv style=display:none>';
                x += addHtmlFormFloating("Unbekanntes Passwort", '<select id=dp20amtbadpass class="form-select"><option value=0>' + "Nichts tun" + '</option><option value=1>' + "Aktivieren Sie in CCM Intel&reg; AMT erneut" + '</option></select>');
                x += '</div>';
                if ((features & 0x400) == 0) { x += addHtmlFormFloating("CIRA-Setup", '<select id=dp20amtcira class="form-select" title="' + "Client Initiated Remote Access" + '"><option value=0>' + "Nichts tun" + '</option><option value=1>' + "Stellen Sie keine Verbindung zum Server her" + '</option><option value=2>' + "Mit Server verbinden" + '</option></select>'); }
                x += '<span id=dp10passNotify style="font-size:10px"> ' + "* 8-16 Zeichen, 1 großes, 1 kleines, 1 numerisches, 1 nicht alphanumerisches Zeichen." + '</span>';
                if ((currentMesh.mtype == 2) && (ptype == 2)) { x += '<span style="font-size:10px"> ' + "Diese Richtlinie hat keine Wirkung auf Intel&reg;-AMT-Geräte im ACM-Modus." + '</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">' + "Wenn diese Richtlinie ausgewählt ist, wird Intel&reg; AMT nicht von diesem Server verwaltet. Intel AMT kann weiterhin verwendet werden, indem es manuell aktiviert und konfiguriert wird." + '</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">' + "Wenn diese Richtlinie ausgewählt ist, wird jeder Intel&reg; AMT im Client Control Mode (CCM) deaktiviert. Bei anderen Geräten wird CIRA gelöscht und kann weiterhin manuell verwaltet werden." + '</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">' + "Dies ist die empfohlene Richtlinie. Die Aktivierung und Verwaltung von Intel&reg; AMT ist vollständig automatisiert und der Server wird versuchen, die Hardwareverwaltung optimal zu nutzen." + '</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("Möchten Sie die Gruppe {0} wirklich löschen? Das Löschen der Gerätegruppe wird auch alle Informationen über die Geräte innerhalb dieser Gruppe löschen.", EscapeHtml(currentMesh.name)) + '<br /><br />';
            x += '<label class=form-check-label><input id=p20check type=checkbox class="form-check-input m-2" onchange=p20validateDeleteMeshDialog() />' + "Bestätigen" + '</label>';
            setModalContent('xxAddAgent', "Gruppe löschen", 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 = "Keine Relaisgeräte verfügbar.";
                setModalContent('xxAddAgent', "Gerätegruppe bearbeiten", "Keine Relaisgeräte verfügbar.");
                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("Relay-Gerät", '<select id=d2devrelay class="form-select">' + relayDevices2.join('') + '</select>');
                setModalContent('xxAddAgent', "Gerätegruppe bearbeiten", 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="' + "Name" + '" onchange=p20editmeshValidate() onkeyup=p20editmeshValidate(event) />';
            x += '<label for=dp20meshname>' + "Name" + '</label>';
            x += '</div>';

            x += '<div class="form-floating mb-2">';
            x += '<textarea id=dp20meshdesc class="form-control" maxlength=1024 placeholder="' + "Beschreibung" + '"></textarea>';
            x += '<label for=dp20meshdesc>' + "Beschreibung" + '</label>';
            x += '</div>';
            setModalContent('xxAddAgent', "Gerätegruppe bearbeiten", 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 = "Gerätegruppen-Nutzereinwilligung bearbeiten"; }
            if (editType == 2) { consent = (currentUser.consent) ? currentUser.consent : 0; title = "Benutzereinwilligung bearbeiten"; }
            if (editType == 3) { consent = (currentNode.consent) ? currentNode.consent : 0; title = "Geräte-Benutzereinwilligung bearbeiten"; }
            if (editType == 4) { consent = (currentUserGroup.consent) ? currentUserGroup.consent : 0; title = "Benutzergruppe Benutzereinwilligung bearbeiten"; }
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px"><b>' + "Desktop" + '</b></div>';
            x += '<div><label for=d20flag1><input type=checkbox id=d20flag1 class="form-check-input me-2" ' + ((consent & 0x0001) ? 'checked' : '') + '>' + "Benutzer benachrichtigen" + '</label></div>';
            x += '<div><label for=d20flag2><input type=checkbox id=d20flag2 class="form-check-input me-2" ' + ((consent & 0x0008) ? 'checked' : '') + '>' + "Aufforderung zur Zustimmung des Benutzers" + '</label></div>';
            x += '<div><label for=d20flag7><input type=checkbox id=d20flag7 class="form-check-input me-2" ' + ((consent & 0x0040) ? 'checked' : '') + '>' + "Datenschutzleiste anzeigen" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Terminal" + '</b></div>';
            x += '<div><label for=d20flag3><input type=checkbox id=d20flag3 class="form-check-input me-2" ' + ((consent & 0x0002) ? 'checked' : '') + '>' + "Benutzer benachrichtigen" + '</label></div>';
            x += '<div><label for=d20flag4><input type=checkbox id=d20flag4 class="form-check-input me-2" ' + ((consent & 0x0010) ? 'checked' : '') + '>' + "Aufforderung zur Zustimmung des Benutzers" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:8px"><b>' + "Dateien" + '</b></div>';
            x += '<div><label for=d20flag5><input type=checkbox id=d20flag5 class="form-check-input me-2" ' + ((consent & 0x0004) ? 'checked' : '') + '>' + "Benutzer benachrichtigen" + '</label></div>';
            x += '<div><label for=d20flag6><input type=checkbox id=d20flag6 class="form-check-input me-2" ' + ((consent & 0x0020) ? 'checked' : '') + '>' + "Aufforderung zur Zustimmung des Benutzers" + '</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' : '') + '>' + "Sitzungen aufzeichnen" + '</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) ? "Synchronisieren Sie den Gerätenamen des Servers mit dem Portnamen" : "Server-Gerätename zu Hostname synchronisieren") + '</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' : '') + '>' + "Wert von --agentName bevorzugen" + '</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' : '') + '>' + "Überschreiben des Server-Gerätenamens bis zur nächsten Verbindung erlauben" + '</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' : '') + '>' + "Gerät nach Trennung entfernen" + '</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' : '') + '>' + "Inaktive Geräte automatisch entfernen" + '</label><br></div>';
            x += '<div style=margin-left:20px id=d20expireDeviceDev>' + "Tage bis zur Entfernung deaktivieren" + ' <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>' + "Standardmäßig werden inaktive Geräte nach 1 Tag entfernt." + '</span>', serverinfo.autoremoveinactivedevices);
                } else {
                    x += format('<span style=font-size:x-small;margin-left:6px>' + "Standardmäßig werden inaktive Geräte nach {0} Tagen entfernt." + '</span>', serverinfo.autoremoveinactivedevices);
                }
            }

            setModalContent('xxAddAgent', "Gerätegruppenmerkmale bearbeiten", 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 += "Erlaube Benutzern, diese Gerätegruppe und die Geräte darin zu verwalten."; } else { x += "Ermöglichen Sie Benutzern die Verwaltung dieses Geräts."; }
                    if (features & 0x00080000) { x += " Benutzer müssen sich an diesem Server anmelden, bevor sie einer Gerätegruppe hinzugefügt werden können." }
                    x += '<br /><br />';
                }
                x += '<div style=\'position:relative\'>';
                x += addHtmlFormFloating("Benutzer-IDs", '<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("Gerätegruppe", '<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("Benutzergruppe", '<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("Benutzergruppe", '<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("Gerätegruppe", '<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("Gerätegruppe", '<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("Gerät", '<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("Gruppenberechtigungen für {0}.", uname) + '<br /><br />';
                } else {
                    x += format("Gruppenberechtigungen für Benutzer {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>' + "Gesamtadministrator" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editmesh>' + "Gerätegruppe bearbeiten" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20manageusers>' + "Gerätegruppen-Benutzer verwalten" + '</label><br>';
                x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20managecomputers>' + "Gerätegruppen-Rechner verwalten" + '</label><br>';
            }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotecontrol>' + "Entfernte Ausführung & Weiterleitung" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remoteview style=margin-left:12px>' + "Nur entfernte Ansicht" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20remotelimitedinput style=margin-left:12px>' + "Nur eingeschränkte Eingabe" + '</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>' + "Mit Gast teilen" + '</label><br>'; }
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nodesktop style=margin-left:12px>' + "Kein Desktop-Zugriff" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noterminal style=margin-left:12px>' + "Kein Terminalzugriff" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20nofiles style=margin-left:12px>' + "Kein Dateizugriff" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20noamt style=margin-left:12px>' + "Kein Intel&reg; AMT" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshagentconsole>' + "Mesh-Agent-Konsole" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20meshserverfiles>' + "Server-Dateien" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20wakedevices>' + "Geräte wecken" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20editnotes>' + "Geräte-Notizen bearbeiten" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20limitevents>' + "Nur eigene Ereignisse anzeigen" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20chatnotify>' + "Chat & Benachrichtigen" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20uninstall>' + "Agent deinstallieren / Gerät löschen" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20commands>' + "Remote-Befehle" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20resetoff>' + "Zurücksetzen / Ausschalten" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20details>' + "Gerätedetails" + '</label><br>';
            x += '<label><input type=checkbox class="form-check-input me-2" onchange=p20validateAddMeshUserDialog() id=p20relay>' + "Als Relay benutzen" + '</label><br>';
            x += '</div>';

            if (userid == null) {
                xxdialogButtons = 3; xxdialogTag = null;
                setModalContent('xxAddAgent', "Benutzer zu Gerätegruppe hinzufügen", 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) ? "Gerätegruppe-Berechtigungen hinzufügen" : "Gerätegruppenberechtigungen bearbeiten", 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', "Benutzergruppe hinzufügen", 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) ? "Gerätegruppe hinzufügen" : "Gerätegruppe bearbeiten", 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) ? "Geräteberechtigungen hinzufügen" : "Geräteberechtigungen bearbeiten", 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) ? "Geräteberechtigungen hinzufügen" : "Geräteberechtigungen bearbeiten", 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 ? "Benutzergeräteberechtigungen bearbeiten" : "Hinzufügen von Benutzergeräteberechtigungen", 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 ? "Benutzergruppen-Geräteberechtigungen bearbeiten" : "Benutzergruppen-Geräteberechtigungen hinzufügen", 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', "Gerätegruppenberechtigungen bearbeiten", x);
                } else {
                    xxdialogButtons = 7, xxdialogTag = userid;
                    setModalContent('xxAddAgent', "Benutzer-Gerätegruppenberechtigungen bearbeiten", 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("Gesamtadministrator (alle Rechte)"); else {
                    if ((meshrights & 1) != 0) r.push("Gerätegruppe bearbeiten");
                    if ((meshrights & 2) != 0) r.push("Gerätegruppen-Benutzer verwalten");
                    if ((meshrights & 4) != 0) r.push("Gerätegruppen-Rechner verwalten");
                    if ((meshrights & 8) != 0) r.push("Fernsteuerung");
                    if ((meshrights & 16) != 0) r.push("Agent-Konsole");
                    if ((meshrights & 32) != 0) r.push("Server-Dateien");
                    if ((meshrights & 64) != 0) r.push("Geräte wecken");
                    if ((meshrights & 128) != 0) r.push("Notizen bearbeiten");
                    if (((meshrights & 8) != 0) && (meshrights & 256) != 0) r.push("Nur entfernte Ansicht");
                    if (((meshrights & 8) != 0) && (meshrights & 65536) != 0) r.push("Kein Desktop");
                    if (((meshrights & 8) != 0) && (meshrights & 512) != 0) r.push("Kein Terminal");
                    if (((meshrights & 8) != 0) && (meshrights & 1024) != 0) r.push("Keine Dateien");
                    if (((meshrights & 8) != 0) && (meshrights & 2048) != 0) r.push("Kein Intel&reg; AMT");
                    if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("Eingeschränkte Eingabe");
                    if ((meshrights & 8192) != 0) r.push("Nur eigene Ereignisse");
                    if ((meshrights & 16384) != 0) r.push("Chat & Benachrichtigen");
                    if ((meshrights & 32768) != 0) r.push("Deinstallation");
                    if ((meshrights & 131072) != 0) r.push("Befehle");
                    if ((meshrights & 262144) != 0) r.push("Zurücksetzen / Aus");
                    if ((meshrights & 524288) != 0) r.push("Teilen");
                    if ((meshrights & 1048576) != 0) r.push("Details");
                    if ((meshrights & 2097152) != 0) r.push("Weiterleitung");
                }
                if (r.length == 0) { r.push("Keine Berechtigungen"); }
                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("Benutzername", EscapeHtml(decodeURIComponent(uname)));
                if (xuserid.split('/')[2] != uname) { x += addHtmlValue("Benutzeridentifikation", EscapeHtml(xuserid.split('/')[2])); }

                x += addHtmlValue("Berechtigungen", r.join(", "));
                if (((userinfo._id) != xuserid) && (cmeshrights == 0xFFFFFFFF || (((cmeshrights & 2) != 0) && (meshrights != 0xFFFFFFFF)))) buttons += 4;
                setModalContent('xxAddAgent', "Gerätegruppenbenutzer", 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', "Benutzerberechtigungen entziehen", format("Bestätigen Sie den Entzug von Rechten für Benutzer \"{0}\"?", EscapeHtml(decodeURIComponent(uname))));
            }
            if (userid.startsWith('ugrp/')) {
                setModalContent('xxAddAgent', "Benutzergruppenberechtigungen entziehen", format("Bestätigen Sie den Entzug von Rechten für Benutzergruppe \"{0}\"?", EscapeHtml(decodeURIComponent(uname))), userid);
            }
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p20viewuserEx2(3, userid));
        }
        function p20deleteUser(e, userid) { haltEvent(e); p20viewuserEx(2, decodeURIComponent(userid)); return false; }
        function p20viewuserEx2(button, userid) { meshserver.send({ action: 'removemeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userid: userid }); }

        function p20editmeshInviteCode() {
            if (xxdialogMode) return false;
            var meshrights = GetMeshRights(currentMesh);

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

            if (meshrights & 1) {
                // We can edit the mesh invite codes
                var x = "Wenn diese Option aktiviert ist, können Einladungscodes von jedem Benutzer verwendet werden, um Geräte über den folgenden öffentlichen Link mit dieser Gerätegruppe zu verbinden:" + '<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() />' + "Einladungscodes aktivieren" + '</label></div>';
                x += addHtmlFormFloating("Codes einladen", '<input id=agentInviteCode class="form-control" onkeyup=p20editmeshInviteCodeValidate() placeholder="code1, code2, code3" />');
                x += addHtmlFormFloating("Agent", '<select id=agentType class="form-select" onchange=p20editmeshInviteCodeValidate()><option value=0>' + "Alle verfügbaren Agents" + '</option><option value=1>' + "Windows MeshAgent" + '</option><option value=2>' + "Linux MeshAgent" + '</option><option value=4>' + "MacOS MeshAgent" + '</option><option value=8>' + "MeshCentral-Assistent" + '</option></select>');
                x += '<div id=d2agentInstallTypeDiv>';
                x += addHtmlFormFloating("Installationstyp", '<select id=agentInviteType class="form-select"><option value=0>' + "Hintergrund und interaktiv" + '</option><option value=2>' + "Nur Hintergrund" + '</option><option value=1>' + "Nur interaktiv" + '</option></select>');
                x += '</div>';
                setModalContent('xxAddAgent', "Codes einladen", 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 = "Einladungscodes können von jedem verwendet werden, um Geräte über den folgenden öffentlichen Link mit dieser Gerätegruppe zu verbinden:" + '<br /><br />';
                x += '<div style=width:100%;text-align:center><a rel="noreferrer noopener" target=_blank href="' + url + '">' + url + '</a></div><br />';
                x += addHtmlValue("Codes einladen", currentMesh.invite.codes.join(', '));
                x += addHtmlValue("Installationstyp", ["Hintergrund und interaktiv", "Nur interaktiv", "Nur Hintergrund"][currentMesh.invite.flags & 3]);
                setModalContent('xxAddAgent', "Codes einladen", 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">' + "Webseiten Benachrichtigung" + '</div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Geräteverbindungen" + '</label></div>';
            x += '<div class="form-check"><label><input id=p20notifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Gerätetrennungen" + '</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 Desktop- und Seriell-Ereignisse" + '</label></div>';
            }
            if (emailNotify) {
                x += '<br /><div style="border-bottom: 1px solid #888;margin-bottom:3px">' + "E-Mail Benachrichtigungen" + '</div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceConnect class="form-check-input me-2" type=checkbox />' + "Geräteverbindungen" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Gerätetrennungen" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20enotifyIntelDeviceHelp class="form-check-input me-2" type=checkbox />' + "Hilfeanforderungen" + '</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 />' + "Geräteverbindungen" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceDisconnect class="form-check-input me-2" type=checkbox />' + "Gerätetrennungen" + '</label></div>';
                x += '<div class="form-check"><label><input id=p20emsgDeviceHelp class="form-check-input me-2" type=checkbox />' + "Hilfeanforderungen" + '</label></div>';
            }

            setModalContent('xxAddAgent', "Benachrichtungseinstellungen", 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: ["Getrennt", "Agent", "Intel&reg; AMT", "Agent + Intel&reg; AMT"] },
                options: { responsive: true, plugins: { legend: { display: false } }, animation: { animateScale: true, animateRotate: true }, layout: { padding: { left: 10, right: 10, top: 10, bottom: 10 } } }
            });
            window.meshSecurityChart = new Chart(document.getElementById('meshSecurityChart').getContext('2d'), {
                type: 'doughnut',
                data: { datasets: [{ data: [0, 0], backgroundColor: ['#20F', '#40D', '#60B', '#809', '#A07', '#C05'] }], labels: ["OK", "Anti-virus ist nicht aktiv", "Kein Automatisches Update", "Firewall nicht aktiv", "Mehrere Probleme"] },
                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>' + "Kein(e)" + '</i>'; }
            if ((meshrights & 1) != 0) { mname = '<span tabindex=0 title="' + "Klicken Sie hier, um den Namen der Gerätegruppe zu bearbeiten" + '" 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>' + "Energiezustände" + '</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>' + "Agent-Typen" + '</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([["Getrennt", "Agent", "Intel&reg; AMT", "Agent + Intel&reg; AMT"][i], connectivityStates[i]]); }
                xconnectivityStates.sort(function (a, b) { return -(a[1] - b[1]) });
                x += '<table class="table table-hover table-striped" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col colspan=2 style=text-align:left;width:430px>' + "Konnektivität" + '</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 /> ' + "Anti-virus ist nicht aktiv",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:noupdate")\' width=10 height=10 /> ' + "Kein Automatisches Update",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:nofirewall")\' width=10 height=10 /> ' + "Firewall nicht aktiv",
                        '<img src="images/link2.png" style=cursor:pointer onclick=\'return p21setDevicesFilter("wsc:any")\' width=10 height=10 /> ' + "Mehrere Probleme"
                    ][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>' + "Sicherheit" + '</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>' + "Keine Geräte in dieser Gerätegruppe." + '</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="' + "Öffentlichen Link anzeigen" + '" onclick=\'return p5showPublicLink("' + (publicPath + '/' + encodeURIComponentEx(f.nx)) + '?download=1' + loginkey + '")\'></i>';
                        publiclink += '<i class="fa-solid fa-download fa-fw" title="' + "Link in Zwischenablage kopieren" + '" 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 "Speicher-Begrenzung überschritten";
            if (bytes < 2048) return format("{0} Bytes verbleibend", bytes);
            if (bytes < 2097152) return format("{0} Kilobytes verbleibend", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("{0} Megabytes verbleibend", Math.round(bytes / 1024 / 1024));
            return format("{0} Gigabytes verbleibend", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function getNiceSize2(bytes) {
            if (bytes <= 0) return "Kein(e)";
            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 "Kein(e)";
            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 in {1} Dateien. {2}k maximal", tf, f.c, (Math.floor(f.maxbytes / 1024 / 1024))); } else { title = format("{0}k in 1 Datei. {1}k maximal", 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', "Öffentlicher Link", '<input type=text style=width:350px value="' + u + '" readonly /> <i class="fa-regular fa-copy" title="' + "Link in Zwischenablage kopieren" + '" 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 ? "Auswahl aufheben" : "Alle auswählen");
            QE('p5CutButton', (sfc > 0) && (cc == sfc));
            QE('p5CopyButton', (sfc > 0) && (cc == sfc));
            QE('p5PasteButton', (p5clipboard != null) && (p5clipboard.length > 0) && (filetreelocation.length > 0));
            [
                ['p5NewFolderButton', 'p5MobileNewFolderButton'],
                ['p5UploadButton', 'p5MobileUploadButton'],
                ['p5DownloadButton', 'p5MobileDownloadButton'],
                ['p5CutButton', 'p5MobileCutButton'],
                ['p5CopyButton', 'p5MobileCopyButton'],
                ['p5PasteButton', 'p5MobilePasteButton']
            ].forEach(function (x) { QE(x[1], !Q(x[0]).disabled); });
        }

        function getFileSelCount(includeDirs) { var cc = 0, checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && ((includeDirs != false) || (checkboxes[i].attributes.file.value == '3'))) cc++; } return cc; }
        function getFileSelDirCount() { var cc = 0, checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && (checkboxes[i].attributes.file.value == '999')) cc++; } return cc; }
        function getFileCount() { var cc = 0; var checkboxes = document.getElementsByName('fc'); return checkboxes.length; }
        function p5selectallfile() { var nv = (getFileSelCount() == 0), checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = nv; } p5setActions(); }
        function setupBackPointers(x) { if (x.f != null) { var fs = 0, fc = 0; for (var i in x.f) { setupBackPointers(x.f[i]); x.f[i].parent = x; if (x.f[i].s) { fs += x.f[i].s; } if (x.f[i].c) { fc += x.f[i].c; } if (x.f[i].t == 3) { fc++; } } x.s = fs; x.c = fc; } return x; }
        function getFileSizeStr(size) { if (typeof size != 'number') { size = 0; } if (size == 1) return "1 Byte"; return format("{0} Bytes", size); }
        function p5folderup(x) { if (x == null) { filetreelocation.pop(); } else { while (filetreelocation.length > x) { filetreelocation.pop(); } } updateFiles(); return false; }
        function p5folderset(x) { filetreelocation.push(decodeURIComponent(x)); updateFiles(); return false; }
        function p5createfolder() {
            setModalContent('xxAddAgent', "Neuer Ordner", '<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>' + "Rekursiv Löschen" + '</label><br>' : '<input type=checkbox id=p5recdeleteinput class="form-check-input me-2" style=\'display:none\'>';
            setModalContent('xxAddAgent', "Löschen", (cc > 1) ? (format("{0} ausgewählte Elemente löschen?", cc) + rec) : ("Ausgewähltes Element löschen?" + 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', "Umbenennen", '<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', "Datei hochladen", '<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\')" />' + "Bestätigen Sie das Überschreiben?" + '</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("Datei-Editor", "Nur Dateien kleiner als 200 kB können bearbeitet werden."); }
                    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("Bestätigen Sie das {0} von {1} Einträgen{2} an diesen Ort?", (p5clipboardCut == 0 ? 'copy' : 'move'), p5clipboard.length, ((p5clipboard.length > 1) ? 's' : '')) }
            setModalContent('xxAddAgent', "Einfügen", 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("Halte {0} Einträge{1} zum {2}", p5clipboard.length, ((p5clipboard.length > 1) ? 's' : ''), (p5clipboardCut == 0 ? "Kopie" : "Move")) + ', <a href=# onclick="return p5clearClip()" style=cursor:pointer>' + "Leeren" + '</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', "Datei hochladen", format((overWriteCount == 1) ? "Das Hochladen wird 1 Datei überschreiben. Fortfahren?" : "Das Hochladen wird {0} Dateien überschreiben. Fortfahren?", 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: "Konto Login",
            2: "Abmelden des Kontos",
            3: "Die Sprache wurde von {1} in {2} geändert.",
            4: "Beitritt zur Desktop-Multiplex-Sitzung", // No longer in use, replaced with 143
            5: "Desktop-Multiplex-Sitzung verlassen", // No longer in use, replaced with 144
            6: "Desktop-Multiplex-Sitzung gestartet", // No longer in use, replaced with 145
            7: "Beendete Aufnahmesitzung, {0} Sekunde(n)", // No longer in use, replaced with 146
            8: "Geschlossene Desktop-Multiplex-Sitzung, {0} Sekunde(n)", // No longer in use, replaced with 147
            9: "Beendete Relay-Sitzung \"{0}\" von {1} zu {2}, {3} Sekunde(n)",
            10: "Beendete Terminalsitzung \"{0}\" von {1} zu {2}, {3} Sekunde(n)",
            11: "Beendet die Desktopsitzung \"{0}\" von {1} zu {2}, {3} Sekunde(n)",
            12: "Beendet die Dateiverwaltungssitzung \"{0}\" von {1} zu {2}, {3} Sekunde(n)",
            13: "Relay-Sitzung \"{0}\" von {1} zu {2} gestartet",
            14: "Terminalsitzung \"{0}\" von {1} zu {2} gestartet",
            15: "Desktop-Sitzung \"{0}\" von {1} zu {2} gestartet",
            16: "Start der Dateiverwaltungssitzung \"{0}\" von {1} zu {2}",
            17: "Konsolenbefehl verarbeiten: \"{0}\"",
            18: "Anzeigen des Meldungsfelds, Titel = \"{0}\", Nachricht = \"{1}\"",
            19: "Beende Prozess {0} ({1})",
            20: "Öffne: {0}",
            21: "Abrufen des Inhalts der Zwischenablage, {0} Byte(s)",
            22: "Festlegen des Inhalts der Zwischenablage, {0} Byte(s)",
            23: "Versuch, den Intel (R) AMT ACM-Modus zu aktivieren",
            24: "Befehle ausführen",
            25: "Power Action ausführen = {0}, Forced = {1}",
            26: "Anzeigen der Toastnachricht, title = \"{0}\", message = \"{1}\"",
            27: "Lokaler Benutzer akzeptiert Remote-Terminal-Anfrage",
            28: "Lokaler Benutzer lehnte Remote-Terminal-Anfrage ab",
            29: "Remotedesktopverbindung wurde vom lokalen Benutzer zwangsweise geschlossen",
            30: "Starten des Remotedesktops nach Annahme durch den lokalen Benutzer",
            31: "Remotedesktop-Verbindungsleiste aktiviert / aktualisiert",
            32: "Remotedesktop-Verbindungsleiste fehlgeschlagen oder nicht unterstützt",
            33: "Remotedesktopverbindung wurde vom lokalen Benutzer zwangsweise geschlossen", // No longer used, 29 must be used instead.
            34: "Fehler beim Starten des Remotedesktops, nachdem der lokale Benutzer abgelehnt hat",
            35: "Remotedesktop mit Toastbenachrichtigung gestartet",
            36: "Remotedesktop ohne Benachrichtigung gestartet",
            37: "Remotedesktop-Verbindungsleiste aktiviert / aktualisiert", // No longer used, 31 must be used instead.
            38: "Remotedesktop-Verbindungsleiste fehlgeschlagen oder nicht unterstützt", // No longer used, 33 must be used instead.
            39: "Remotedesktopverbindung wurde vom lokalen Benutzer zwangsweise geschlossen", // No longer used, 29 must be used instead.
            40: "Starten von Remote-Dateien, nachdem der lokale Benutzer dies akzeptiert hat",
            41: "Remote-Dateien konnten nicht gestartet werden, nachdem der lokale Benutzer abgelehnt hat",
            42: "Remote-Dateien mit Toast-Benachrichtigung gestartet",
            43: "Remote-Dateien ohne Benachrichtigung gestartet",
            44: "Ordner erstellen: \"{0}\"",
            45: "Löschen: \"{0}\"",
            46: "Rekursiv löschen: \"{0}\", {1} Element(e) entfernt",
            47: "Löschen: \"{0}\", {1} Element(e) entfernt",
            48: "Umbenennen: \"{0}\" in \"{1}\"",
            49: "Download: \"{0}\"",
            50: "Hochladen: \"{0}\"",
            51: "Kopieren: \"{0}\" nach \"{1}\"",
            52: "Verschieben: \"{0}\" nach \"{1}\"",
            53: "Remote-Benutzer vom Desktop sperren",
            54: "Agent hat Sitzung mit {0}% Agent-zu-Server-Komprimierung geschlossen. Gesendet: {1}, komprimiert: {2}",
            55: "Erstellte Gerätegruppe: {0}",
            56: "Gerätegruppe nicht gelöscht: {0}",
            57: "Gerät {0} zur Gerätegruppe {1} hinzugefügt",
            58: "Gerät hat Intel (R) AMT ACM-Aktivierung angefordert, FQDN: {0}",
            59: "Gerät {0} von Gruppe {1} geändert: {2}",
            60: "Benutzergeräte-Rechte für {0} entfernt",
            61: "Geänderte Benutzergeräterechte für {0}",
            62: "Benutzer {0} wurde aus Benutzergruppe {1} entfernt",
            63: "Konto entfernt",
            64: "Konto erstellt, Benutzername ist {0}",
            65: "Konto erstellt, E-Mail ist {0}",
            66: "Konto geändert: {0}",
            67: "Benutzergruppenmitgliedschaft geändert: {0}",
            68: "Benutzergruppe {0} zur Gerätegruppe {1} hinzugefügt",
            69: "Benutzergruppe erstellt: {0}",
            70: "Benutzergruppe {0} aus Gerätegruppe {1} entfernt",
            71: "Benutzer {0} zur Benutzergruppe {1} hinzugefügt",
            72: "Benutzer {0} wurde aus Benutzergruppe {1} entfernt",
            73: "Gerätegruppenbenachrichtigung geändert",
            74: "Kontokennwort geändert: {0}",
            75: "Kontodaten geändert",
            76: "Gerätegruppe erstellt: {0}",
            77: "Gerätegruppe gelöscht: {0}",
            78: "Gerätegruppenmitgliedschaft geändert: {0}",
            79: "Benutzergruppe geändert: {0}",
            80: "Benutzer {0} zur Benutzergruppe {1} hinzugefügt",
            81: "Benutzergeräte-Rechte für {0} entfernt",
            82: "Geänderte Benutzergeräterechte für {0}",
            83: "Benutzer {0} aus Gerätegruppe {1} entfernt",
            84: "Gerät {0} zur Gerätegruppe {1} hinzugefügt",
            85: "Gerät {0} in Gruppe {1} verschoben",
            86: "Benutzergeräte-Rechte für {0} entfernt",
            87: "Gerät {0} aus Gerätegruppe {1} entfernt",
            88: "Aktivierte E-Mail-Zwei-Faktor-Authentifizierung",
            89: "E-Mail Zwei-Faktor-Authentifizierung deaktiviert",
            90: "Authentifizierungsanwendung hinzugefügt",
            91: "Authentifizierungsanwendung entfernt",
            92: "Neue 2FA-Sicherungscodes generiert",
            93: "2FA-Sicherungscodes gelöscht",
            94: "Sicherheitsschlüssel entfernt",
            95: "Sicherheitsschlüssel hinzugefügt",
            96: "Verifizierte Telefonnummer des Benutzers {0}",
            97: "Telefonnummer des Benutzers {0} entfernt",
            98: "Angeforderte Hilfe, Benutzer: {0}, Details: {1}",
            99: "Befehle als Benutzer ausführen",
            100: "Wenn möglich, Befehle als Benutzer ausführen",
            101: "Gerätefreigabe {0} von {1} zu {2} hinzugefügt",
            102: "Gerätefreigabe {0} entfernt",
            103: "Batch-Upload von {0} Datei (en) in Ordner {1}",
            104: "Automatischer Download der Agent Core Dump-Datei: \"{0}\"",
            105: "Hochladen: \"{0}\", Größe: {1}",
            106: "Download: \"{0}\", Größe: {1}",
            107: "Konto Login von {0}, {1}, {2}",
            108: "Benutzeranmeldeversuch mit falschem 2. Faktor von {0}, {1}, {2}",
            109: "Benutzeranmeldeversuch für gesperrtes Konto von {0}, {1}, {2}",
            110: "Ungültiger Anmeldeversuch von",
            111: "Gerät angefordert Intel(R) AMT ACM TLS activation, FQDN: {0}",
            112: "Beendete Messenger-Sitzung \"{0}\" von {1} zu {2}, {3} Sekunde(n)",
            113: "Authentifizierungsgerät für Push-Benachrichtigungen hinzugefügt",
            114: "Authentifizierungsgerät für Push-Benachrichtigungen entfernt",
            115: "Login-Token hinzugefügt",
            116: "Login-Token entfernt",
            117: "Dies ist eine alte Agent-Version. Ziehen Sie eine Aktualisierung in Betracht.",
            118: "Dieser Agent verfügt über einen veralteten Zertifikatsvalidierungsmechanismus. Ziehen Sie eine Aktualisierung in Betracht.",
            119: "Dieser Agent verwendet unsichere Tunnel. Ziehen Sie eine Aktualisierung in Betracht.",
            120: "Lokale Relay-Sitzung \"{0}\" gestartet, Protokoll {1} ​​zu {2}",
            121: "Beendete lokale Relay-Sitzung \"{0}\", Protokoll {1} ​​zu {2}, {3} Sekunde(n)",
            122: "Desktop-Multiplex-Sitzung nach {0} Sekunde(n) verlassen.", // No longer in use, replaced with 144
            123: "Web-SSH-Sitzung \"{1}\" nach {0} Sekunde(n) verlassen.",
            124: "Web-SFTP-Sitzung \"{1}\" nach {0} Sekunde(n) verlassen.",
            125: "Web-RDP-Sitzung \"{1}\" nach {0} Sekunde(n) verlassen.",
            126: "Web-VNC-Sitzung nach {0} Sekunde(n) verlassen.",
            127: "Anzeigename des Kontos in {0} geändert.",
            128: "Konto erstellt, Name ist {0}.",
            129: "Anzeigename des Kontos entfernt.",
            130: "Benutzer Benachrichtigung geändert",
            131: "Gerätefreigabe {0} mit unbegrenzter Zeit hinzugefügt.",
            132: "Anmachen.",
            133: "Schalte aus.",
            134: "Desktop-Sitzung von Benutzer {0} zwangsweise getrennt",
            135: "Terminalsitzung von Benutzer {0} zwangsweise getrennt",
            136: "Dateisitzung von Benutzer {0} zwangsweise getrennt",
            137: "Routing-Sitzung von Benutzer {0} zwangsweise getrennt",
            138: "Täglich wiederkehrende Gerätefreigabe {0} hinzugefügt.",
            139: "Wöchentlich wiederkehrende Gerätefreigabe {0} hinzugefügt.",
            140: "Gerät {0} von Gruppe {1} geändert: {2}",
            141: "Intel(r) AMT-Richtlinienänderung",
            142: "Gerätegruppe {0} wurde geändert: {1}",
            143: "Desktop-Multiplex-Sitzung beigetreten \"{0}\"",
            144: "Desktop-Multiplex-Sitzung \"{0}\" nach {1} Sekunde(n) verlassen.",
            145: "Desktop-Multiplex-Sitzung gestartet \"{0}\"",
            146: "Beendete Aufzeichnungssitzung \"{0}\", {1} Sekunde(n)",
            147: "Geschlossene Desktop-Multiplex-Sitzung »{0}«, {1} Sekunde(n)",
            148: "Web-SSH Sitzung \"{0}\" gestartet.",
            149: "Web-SFTP Sitzung \"{0}\" gestartet.",
            150: "Web-RDP Sitzung \"{0}\" gestartet.",
            151: "Web-VNC Sitzung \"{0}\" gestartet.", // Not in use yet
            152: "Kein Relay mehr für \"{0}\".",
            153: "Ist ein Relais für \"{0}\".",
            154: "Konto geändert, um mit LDAP-Daten zu synchronisieren.",
            155: "Login des Benutzers von {0}, {1}, {2} verweigert",
            156: "Verifizierter Benachrichtigungsaccount des Benutzers {0}",
            157: "Benachrichtigen für den Benutzer {0} entfernt",
            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: "Messenger-Sitzung \"{0}\" von {1} zu {2} gestartet",
            163: "Gerät {0} aus der Benutzergruppe {1} entfernt",
            164: "Datei erstellen: \"{0}\""
        };

        var eventsShortMessageId = {
            1: "Gruppenname",
            2: "Beschreibung",
            3: "Flaggen",
            4: "Zustimmung",
            5: "Auto-Entfernen",
            6: "Code einladen",
            7: "Relaisgerät"
        }

        // 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="' + "Dies ist eine Gast-Sharing-Sitzung" + '">' + 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>' + "Keine Ereignisse vorhanden" + '</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("Berichtstyp", '<select id=d2reportType class="form-select" onchange=p3showReportValidation()><option value=1>' + "Alle Ereignisse" + '</option>');
            x += addHtmlFormFloating("Zeitspanne", '<select id=d2reportSpan class="form-select" onchange=p3showReportValidation()><option value=1>' + "Alles Verfügbar" + '</option><option value=2>' + "Eines Tages" + '</option></select>');
            x += '<div id=d2daySelector>' + addHtmlFormFloating("Berichtstag", '<input type=date id=d2reportDay class="form-control" onchange=p3showReportValidation()></input>') + '</div>';
            x += addHtmlFormFloating("Gruppiere nach", '<select id=d2reportGroupBy class="form-select" onchange=p3showReportValidation()><option value=1>' + "Kein(e)" + '</option><option value=2>' + "Gerätegruppe" + '</option><option value=3>' + "Benutzer" + '</option>');
            x += addHtmlFormFloating("Format", '<select id=d2reportFormat class="form-select" onchange=p3showReportValidation()><option value=1>' + "CSV" + '</option><option value=2>' + "JSON" + '</option>');
            x += '</div>';
            setModalContent('xxAddAgent', "Bericht herunterladen", 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 = "Die Ereignisliste in einem der folgenden Dateiformate herunterladen." + '<br /><br />';
            x += addHtmlValue("CSV-Format", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogCSV(' + mode + ')">' + 'eventslist.csv' + '</a>');
            x += addHtmlValue("JSON-Format", '<a href=# style=cursor:pointer onclick="return p3downloadEventsDialogJSON(' + mode + ')">' + 'eventslist.json' + '</a>');
            setModalContent('xxAddAgent', "Ereignisliste exportieren", 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, Zeit, Typ, Aktion, Benutzer, Gerät, Nachricht" + '\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>' + "Name" + '<th style=width:80px>' + "Gerätegruppen" + '<th style=width:120px>' + nobreak("Letzter Zugriff") + '<th style=width:120px>' + "Berechtigungen";

            // 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>' + "Benutzer online"; 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>' + "Benutzer offline"; addHeader = false; }
                        x += addUserHtml(user, sessions);
                        maxUsers--;
                    } else {
                        hiddenUsers++;
                    }
                }
            }
            x += '</table>';
            if (hiddenUsers == 1) { x += '<br />' + "1 weiterer Benutzer nicht angezeigt, Suchfeld verwenden um weitere Benutzer zu suchen..." + '<br />'; }
            else if (hiddenUsers > 1) { x += '<br />' + format("{0} weitere Benutzer nicht angezeigt, Suchfeld verwenden um weitere Benutzer zu suchen...", hiddenUsers) + '<br />'; }
            if (maxUsers == displayUsersLimit) { x += '<br />' + "Keine Benutzer gefunden." + '<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 Sitzung"); } else { lastAccess += nobreak(format("{0} Sitzungen", sessions)); }
            } else {
                if (user.access) { lastAccess += '<span title="' + format("Letzter Zugriff: {0}", printDateTime(new Date(user.access * 1000))) + '">' + printDate(new Date(user.access * 1000)) + '</span>'; }
                else if (user.login) { lastAccess += '<span title="' + format("Letzte Anmeldung: {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 += "Gesperrt" + ',&nbsp;'; }
            permissions += '<span title=\'' + "Server-Berechtigungen" + '\'>';

            var urights = user.siteadmin & (0xFFFFFFFF - 1248);
            if ((user.siteadmin == null) || (urights == 0)) {
                permissions += "Benutzer";
            } else if (urights == 8) {
                permissions += "Benutzer + Dateien";
            } else if (user.siteadmin == 0xFFFFFFFF) {
                permissions += "Administrator";
            } else if ((urights & 2) != 0) {
                permissions += "Verwalter";
            } else {
                permissions += "Partiell";
            }
            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="' + "E-Mail ist nicht verifiziert" + '">&#x2717;</b>' : ' <b style=color:green title="' + "E-Mail wird überprüft" + '">&#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="' + "E-Mail an Benutzer senden" + '" 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="' + "Zweifaktorauthentifizierung eingeschaltet" + '" style="margin-top:2px"></i>'; }
            if (user.phone != null) { username += ' <i class="fa-solid fa-mobile-screen" title="' + "Verifizierte Telefonnummer" + '" 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="' + "Konto ist gesperrt" + '" style="margin-top:2px" />'; }
            if ((user.msghandle != null) && (features2 & 0x02000000)) { username += ' <i class="fa-solid fa-comment fa-xs" title="' + "Verifizierter Benachrichtigungsaccount" + '" 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) ? "Auswahl aufheben" : "Alle auswählen";
        }

        // 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>' + "E-Mail validieren" + '</option><option value=5>' + "E-Mail ungültig machen" + '</option>'; }

            var x = "Wählen Sie eine Operation für die Durchführung auf alle ausgewählten Benutzer." + '<br /><br />';
            x += addHtmlFormFloating("Vorgang", '<select id=d3groupop class=form-select><option value=1>' + "Konto sperren" + '</option><option value=2>' + "Konto entsperren" + '</option>' + optionalActions + '<option value=3>' + "Konto löschen" + '</option></select>');
            setModalContent('xxAddAgent', "Gruppenaktion", 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 = "Bestätigen Sie das Löschen der gewählten Konten?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p3usersGroupActionFunctionDelCheck() />' + "Bestätigen" + '</label>';
                setModalContent('xxAddAgent', "Konten löschen", x);
                showModal('xxAddAgentModal', 'idx_dlgOkButton', p3groupActionFunctionDelExec);
                QE('idx_dlgOkButton', false);
                return false;
            } else if (op == 4) {
                // Validate emails
                for (var i in userids) {
                    var user = users[userids[i]];
                    if (user.emailVerified !== true) { meshserver.send({ action: 'edituser', id: user._id, emailVerified: true }); }
                }
            } else if (op == 5) {
                // Invalidate emails
                for (var i in userids) {
                    var user = users[userids[i]];
                    if (user.emailVerified === true) { meshserver.send({ action: 'edituser', id: user._id, emailVerified: false }); }
                }
            }
        }

        function p3usersGroupActionFunctionDelCheck() { QE('idx_dlgOkButton', Q('d3check').checked); }

        // Delete a batch of user accounts
        function p3groupActionFunctionDelExec(b) {
            var elements = document.getElementsByClassName('UserCheckbox'), userids = [];
            for (var i = 0; i < elements.length; i++) { if (elements[i].checked === true) { userids.push(decodeURIComponent(elements[i].value)); } }
            for (var i in userids) { var user = users[userids[i]]; meshserver.send({ action: 'deleteuser', userid: user._id, username: user.name }); }
        }

        // Highlights the user being hovered
        function userMouseHover(element, over) {
            var e = element.children[0].children[0].children[1];
            e.children[1].classList.remove('g1s');
            e.children[2].classList.remove('g2s');
            element.children[0].children[0].classList.remove('sbar');
            if (over == 1) {
                e.children[1].classList.add('g1s');
                e.children[2].classList.add('g2s');
                element.children[0].children[0].classList.add('sbar');
            }
        }

        // Highlights the user being hovered
        function userMouseHover2(element, over) {
            var e = element.children[0].children[0];
            e.children[2].classList.remove('g1s');
            e.children[3].classList.remove('g2s');
            element.children[0].children[0].classList.remove('sbar');
            if (over == 1) {
                e.children[2].classList.add('g1s');
                e.children[3].classList.add('g2s');
                element.children[0].children[0].classList.add('sbar');
            }
        }

        function userChat(e, userid, name) {
            if (xxdialogMode) return;
            haltEvent(e);
            var url = '/messenger?id=meshmessenger/' + userid + '/' + encodeURIComponentEx(userinfo._id) + '&title=' + name;
            if ((authCookie != null) && (authCookie != '')) { url += '&auth=' + authCookie; }
            if (urlargs.key) { url += '&key=' + urlargs.key; }
            safeNewWindow(url, 'meshmessenger:' + userid);
            meshserver.send({ action: 'meshmessenger', userid: decodeURIComponent(userid) });
            return false;
        }

        function altUserChat(e, userid, name, i) {
            if (xxdialogMode) return;
            haltEvent(e);
            var url = serverinfo.altmessenging[i].url;
            var ruserid = decodeURIComponent(userid);
            var userid1 = encodeURIComponentEx(ruserid.split('/')[2]); // userid
            var userid2 = encodeURIComponentEx(ruserid.split('/').join('-')); // user-domain-userid
            var userid3 = userid1, userid4 = userid2;
            var ruser = users[ruserid];
            if ((ruser != null) && (ruser.realname != null)) {
                userid3 = encodeURIComponentEx(ruser.realname.split(' ').join('')); // real name with no empty spaces
                userid4 = encodeURIComponentEx(ruser.realname.split(' ').join('-')); // real name with - instead of spaces
            }
            url = url.split('{0}').join(userid1).split('{1}').join(userid2).split('{2}').join(userid3).split('{3}').join(userid4);
            if (urlargs.key) { url += '&key=' + urlargs.key; }
            safeNewWindow(url, 'altmessenger:' + userid);
            meshserver.send({ action: 'notifyuser', userid: decodeURIComponent(userid), msg: serverinfo.altmessenging[i].name, msgid: 11, url: url });
            return false;
        }

        function showSendSMS(userid) {
            if (xxdialogMode) return;
            setModalContent('xxAddAgent', "SMS senden", '<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', "Nachricht senden", '<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="' + "Gegenstand" + '"></input>';
            x += '<textarea id=d2emailText maxlength=10000 style="width:100%;height:200px;resize:none;overflow-y:auto" onKeyUp=showSendEmailValidate()></textarea>';

            setModalContent('xxAddAgent', "E-Mail senden", 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>' + "Eine Textbenachrichtigung an diesen Benutzer senden." + '</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>' + "Nachricht anzeigen, bis sie vom Benutzer geschlossen wird" + '</option>';
            x += '<option value=10>' + "10 Sekunden lang anzeigen" + '</option>';
            x += '<option value=60>' + "1 Minute lang zeigen" + '</option>';
            x += '<option value=300>' + "5 Minuten lang anzeigen" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', format("{0} benachrichtigen", 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') + '>' + "Nur die ersten 100 Benutzer anzeigen" + '</label><br />';
            setModalContent('xxAddAgent', "Benutzeransicht", 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 = "Erzeuge viele Konten auf einmal durch Import einer JSON oder CSV-Datei" + '<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', "Benutzerkonten-Import", 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', "Benutzerkonten-Import", "Ungültiges CSV-Dateiformat.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Benutzerkonten-Import", "Ungültiges CSV-Dateiformat.");
                    }
                } else {
                    try {
                        j = JSON.parse(r.target.result);
                    } catch (ex) {
                        setModalContent('xxAddAgent', "Benutzerkonten-Import", format("Ungültige JSON-Datei: {0}.", ex));
                        showModal('xxAddAgentModal', 'idx_dlgOkButton');
                        return;
                    }
                    if ((j != null) && (Array.isArray(j))) {
                        var ok = true;
                        for (var i in j) {
                            if ((typeof j[i].user != 'string') || (j[i].user.length < 1) || (j[i].user.length > 64)) { ok = false; }
                            if ((typeof j[i].pass != 'string') || (j[i].pass.length < 1) || (j[i].pass.length > 256)) { ok = false; }
                            if (checkPasswordRequirements(j[i].pass, passRequirements) == false) { ok = false; }
                            if ((j[i].email != null) && ((typeof j[i].email != 'string') || (j[i].email.length < 1) || (j[i].email.length > 128))) { ok = false; }
                        }
                        if (ok == false) {
                            setModalContent('xxAddAgent', "Benutzerkonten-Import", "Ungültiges JSON-Dateiformat.");
                        } else {
                            meshserver.send({ action: 'adduserbatch', users: j });
                        }
                    } else {
                        setModalContent('xxAddAgent', "Benutzerkonten-Import", "Ungültiges JSON-Dateiformat.");
                    }
                    showModal('xxAddAgentModal', 'idx_dlgOkButton');
                }
            };
            fr.readAsText(Q('d4importFile').files[0]);
        }

        function p4downloadUserInfo() {
            if (xxdialogMode) return;
            var x = "Die Benutzerliste in einem der folgenden Dateiformate herunterladen." + '<br /><br />';
            x += addHtmlValue("CSV-Format", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoCSV()\'>' + 'userlist.csv' + '</a>');
            x += addHtmlValue("JSON-Format", '<a href=# style=cursor:pointer onclick=\'return p4downloadUserInfoJSON()\'>' + 'userlist.json' + '</a>');
            setModalContent('xxAddAgent', "Benutzerlistenexport", x);
            showModal('xxAddAgentModal', 'idx_dlgOkButton');
        }

        function p4downloadUserInfoCSV() {
            var csv = "ID,Name,E-Mail,Erstellung,letzte Anmeldung,Gruppen,Authfactors,Siteadmin,Benutzeradmin,gesperrt" + '\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>' + "Eine Nachricht an alle verbundenen Benutzer senden." + '</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>' + "Nachricht anzeigen, bis sie vom Benutzer geschlossen wird" + '</option>';
            x += '<option value=10>' + "10 Sekunden lang anzeigen" + '</option>';
            x += '<option value=60>' + "1 Minute lang zeigen" + '</option>';
            x += '<option value=300>' + "5 Minuten lang anzeigen" + '</option>';
            x += '</select>';
            setModalContent('xxAddAgent', "Rundruf-Nachricht", 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] == '') ? "Standard" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Domain", y);
            }

            if ((features & 0x200000) == 0) { x += addHtmlFormFloating('<span id=p4hname>' + "Benutzername" + '</span>', '<input id=p4name class="form-control" maxlength=64 autocomplete=username onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />'); }
            x += addHtmlFormFloating('<span id=p4hemail>' + "E-Mail", '<input id=p4email type=email class="form-control" maxlength=256 autocomplete="email" inputmode="email" onchange=showCreateNewAccountDialogValidate() onkeyup=showCreateNewAccountDialogValidate() />');
            x += '';
            x += addHtmlFormFloating('<span id=p4hp1>' + "Passwort" + '</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" />' + "Das Passwort randomisieren." + '</label></div>';
            x += '<div><label><input id=p4removeEvents onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Alle vorhergehenden Ereignisse für diese Benutzer-ID entfernen." + '</label></div>';
            x += '<div><label><input id=p4resetNextLogin onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "Passwort beim nächsten Login zurücksetzen." + '</label></div>';
            if (serverinfo.emailcheck) {
                x += '<div><label><input id=p4verifiedEmail onchange=showCreateNewAccountDialogValidate() type=checkbox class="form-check-input me-2" />' + "E-Mail ist bestätigt." + '</label></div>';
                x += '<div><label><input id=p4invitationEmail type=checkbox class="form-check-input me-2" title=' + "E-Mail-verifiziert und erzwungenes Zurücksetzen des Passworts erforderlich." + ' />' + "Sende Einladungs-E-Mail." + '</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("Voraussetzungen: {0}.", r.join(', ')) + '</div>'; }
            }

            setModalContent('xxAddAgent', "Konto erstellen", 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 = "Geben Sie eine durch Kommas getrennte Liste der Namen der Verwaltungsbereiche ein." + '<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', "Administrative Bereiche", 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>' + "Server-Dateien" + '</label>, <input type=number class="me-1 form-control" onchange=showUserAdminDialogValidate() maxlength=10 id=ua_fileaccessquota>' + "k max, leer für Standard" + '</div><br><hr/>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_fulladmin>' + "Gesamtadministrator" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverbackup>' + "Server-Datensicherung" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverrestore>' + "Server-Wiederherstellung" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_serverupdate>' + "Server-Aktualisierungen" + '</label><br></div>';
            x += '<div id=d2MngUsr><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusers>' + "Benutzer verwalten" + '</label><br></div>';
            x += '<div id=d2MngGrp><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_manageusergroups>' + "Benutzergruppen verwalten" + '</label><br></div>';
            x += '<div id=d2MngRec><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_managerecordings>' + "Aufnahmen verwalten" + '</label><br></div>';
            x += '<div id=d2AllEvnt><label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_allevents>' + "Alle Ereignisse anzeigen" + '</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>' + "Konto sperren" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewgroups>' + "Keine neuen Gerätegruppen" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nonewdevices>' + "Keine neuen Geräte" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_nomeshcmd>' + "Keine Werkzeuge (MeshCmd/Router)" + '</label><br>';
            x += '<label><input type=checkbox onchange=showUserAdminDialogValidate() class="form-check-input me-2" id=ua_locksettings>' + "Kontoeinstellungen sperren" + '</label><br>';
            x += '</div>';

            setModalContent('xxAddAgent', "Server-Berechtigungen", 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 />' + "Keine Gruppen gefunden." + '<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>' + "Name" + '<th style=width:80px>' + "Benutzer" + '<th style=width:80px>' + "Gerätegruppen" + '<th style=width:80px>' + "Geräte";
                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) ? "Auswahl aufheben" : "Alle auswählen";
        }

        // 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 = "Wählen Sie eine Operation für die Durchführung auf alle ausgewählten Benutzer." + '<br /><br />';
            x += addHtmlFormFloating("Vorgang", '<select id=d50groupop class=form-select><option value=1>' + "Gruppe löschen" + '</option></select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Gruppenaktion", 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 = "Bestätigen Sie das Löschen der gewählten Benutzergruppen?" + '<br /><br />';
                x += '<label><input id=d3check type=checkbox class="form-check-input me-2" onchange=p50usersGroupActionFunctionDelCheck() />' + "Bestätigen" + '</label>';
                setModalContent('xxAddAgent', "Benutzergruppen löschen", 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("Benutzergruppe", '<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] == '') ? "Standard" : EscapeHtml(serverinfo.crossDomain[i])) + '</option>'; }
                y += '</select>';
                x += addHtmlFormFloating("Domain", y);
            }
            x += addHtmlFormFloating("Name", '<input id=p4name class=form-control maxlength=64 onchange=showCreateUserGroupDialogValidate() onkeyup=showCreateUserGroupDialogValidate() />');
            x += addHtmlFormFloating("Beschreibung", '<textarea id=p4desc class=form-control value="" maxlength=1024 /></textarea>');
            setModalContent('xxAddAgent', (mode == 1) ? "Benutzergruppe erstellen" : "Dupliziere Benutzergruppe", 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>' + "Kein(e)" + '</i>'; }
            if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { gname = '<span role=button tabindex=0 title="' + "Klicken Sie hier, um den Benutzergruppennamen zu bearbeiten" + '" 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>' + "Kein(e)" + '<i>'; } else { desc = EscapeHtml(desc); }

            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Name", 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("Domain", (d != '') ? EscapeHtml(d) : ('<i>' + "Standard" + '</i>'));
                x += addDeviceAttribute("Gruppen-ID", EscapeHtml(group._id));
            }
            if (currentUserGroup.membershipType != null) {
                x += addDeviceAttribute("Gruppentyp", EscapeHtml(currentUserGroup.membershipType));
            }
            if ((userinfo.siteadmin & 256) != 0) {
                x += addDeviceAttribute("Beschreibung", '<span role=button onclick=p51editgroup(2,' + (currentUserGroup.membershipType != null) + ')>' + desc + ' <i class="fa-solid fa-pencil fa-xs"/></i></span>');
            } else {
                x += addDeviceAttribute("Beschreibung", desc);
            }

            // Display features
            if (serverinfo.userGroupsSessionRecording == 1) {
                var userGroupFeatures = [];
                if ((group.flags) && (group.flags & 2)) { userGroupFeatures.push("Sitzungen aufzeichnen"); }
                userGroupFeatures = userGroupFeatures.join(', ');
                if (userGroupFeatures == '') { userGroupFeatures = '<i>' + "Kein(e)" + '</i>'; }
                x += addDeviceAttribute("Eigenschaften", 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("Desktop-Einverständnis & Datenschutzleiste"); } else if (consent & 0x0040) { consentOptionsStr.push("Desktop-Werkzeugleiste"); } else if (consent & 0x0008) { consentOptionsStr.push("Desktop-Einverständnis"); } else { if (consent & 0x0001) { consentOptionsStr.push("Desktop-Benachrichtigung"); } }
                if (consent & 0x0010) { consentOptionsStr.push("Terminal-Einverständnis"); } else { if (consent & 0x0002) { consentOptionsStr.push("Terminal-Benachrichtigung"); } }
                if (consent & 0x0020) { consentOptionsStr.push("Dateien-Einverständnis"); } else { if (consent & 0x0004) { consentOptionsStr.push("Dateien-Benachrichtigung"); } }
                if (consent == 7) { consentOptionsStr = ["Immer benachrichtigen"]; }
                if ((consent & 56) == 56) { consentOptionsStr = ["Immer nachfragen"]; }

                consentOptionsStr = consentOptionsStr.join(', ');
                if (consentOptionsStr == '') { consentOptionsStr = '<i>' + "Kein(e)" + '</i>'; }
                x += addDeviceAttribute("Benutzereinwilligung", addLinkConditional(consentOptionsStr, 'p20editmeshconsent(4)', true));
            }

            x += addDeviceAttribute("Benutzer", usercount);
            x += addDeviceAttribute("Gerätegruppen", meshcount);
            x += addDeviceAttribute("Geräte", devicecount);

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

            if ((userinfo.siteadmin & 256) != 0) {
                x += '<input type=button class="btn btn-primary btn-sm" value="' + "Rundruf" + '" title="' + "Eine Mitteilung an alle Benutzer in dieser Gruppe senden." + '" 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> ' + "Benutzer hinzufügen" + '</button>';
            }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Gruppenmitglieder" + '</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="' + "Entziehe Benutzerrechte an dieser Gerätegruppe" + '" 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="' + "Benutzer" + '"></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>' + "Keine Mitglieder" + '</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> ' + "Gerätegruppe hinzufügen" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Gemeinsame Gerätegruppen" + '</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>' + "Unbekannte Gerätegruppe" + '</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="' + "Benutzergruppenrechte an dieser Gerätegruppe entfernen" + '" 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="' + "Gerätegruppe" + '" 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>' + "Keine gemeinsamen Gerätegruppen" + '</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> ' + "Gerät hinzufügen" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Allgemeine Geräte" + '</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>' + "Unbekanntes Gerät" + '</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="' + "Benutzergruppenrechte an diesem Gerät entziehen" + '" 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="' + "Gerätegruppe" + '" 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>' + "Keine gemeinsamen Geräte" + '</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>' + "Benutzergruppe löschen" + '</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' : '') + '>' + "Sitzungen aufzeichnen" + '</label><br></div>';
            }
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Benutzergruppenfunktionen bearbeiten", 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', "Geräteberechtigungen entfernen", format("Bestätigen Sie den Entzug von Zugriffsrechten für Gerät \"{0}\"?", EscapeHtml(node.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => p51removeDeviceFromUserGroupEx(3, node._id));
        }

        function p51removeDeviceFromUserGroupEx(b, nodeid) {
            meshserver.send({ action: 'adddeviceuser', nodeid: nodeid, userids: [currentUserGroup._id], rights: 0, remove: true });
        }

        function p51removeMeshFromUserGroup(e, meshid) {
            if (xxdialogMode) return;
            var mesh = meshes[decodeURIComponent(meshid)];
            if (mesh == null) return;
            setModalContent('xxAddAgent', "Gerätegruppenberechtigungen entfernen", format("Bestätigen Sie den Entzug von Zugriffsrechten für Gerätegruppe  \"{0}\"?", EscapeHtml(mesh.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => { meshserver.send({ action: 'removemeshuser', meshid: decodeURIComponent(meshid), userid: currentUserGroup._id }); });;
        }
        function p51editgroup(focus, nameReadOnly) {
            if (xxdialogMode) return;
            var x = addHtmlFormFloating("Name", '<input id=dp51name class="form-control" maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) ' + (nameReadOnly ? 'readonly disabled' : '') + '/>');
            x += addHtmlFormFloating("Beschreibung", '<textarea id=dp51desc class="form-control" maxlength=1024 style=width:100%;resize:none></textarea>');

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Benutzergruppe bearbeiten", 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("Benutzergruppe {0} löschen?", EscapeHtml(currentUserGroup.name)) + '<br /><br />';
            x += '<label><input id=p51check type=checkbox class="form-check-input me-2" onchange=p51validateDeleteGroupDialog() />' + "Bestätigen" + '</label>';

            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Benutzergruppe löschen", 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', "Benutzermitgliedschaft entziehen", format("Bestätigen Sie den Mitgliedschaftsentzug für Benutzer \"{0}\"?", EscapeHtml(decodeURIComponent(uname))));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', () => { meshserver.send({ action: 'removeuserfromusergroup', ugrpid: currentUserGroup._id, userid: userid }); });
        }

        function p51showAddUserDialog() {
            if (xxdialogMode) return false;
            var x = "Erlaube Benutzern, diese Gerätegruppe und die Geräte darin zu verwalten.";
            if (features & 0x00080000) { x += " Benutzer müssen sich an diesem Server anmelden, bevor sie einer Gerätegruppe hinzugefügt werden können." }
            x += '<br /><br /><div style=\'position:relative\'>';
            x += addHtmlFormFloating("Benutzer-IDs", '<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', "Benutzer zu Benutzergruppe hinzufügen", 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="' + "Konto ist gesperrt" + '" style="margin-top:2px" /> '; msg.push("Gesperrtes Konto"); }
            if ((user.siteadmin == null) || ((user.siteadmin & (0xFFFFFFFF - 1248)) == 0)) { msg.push("Keine Serverberechtigungen"); } else if (user.siteadmin == 8) { msg.push("Zugriff auf Server-Dateien"); } else if (user.siteadmin == 0xFFFFFFFF) { msg.push("Gesamtadministrator"); } else { msg.push("Partielle Rechte"); }
            if ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & (64 + 128 + 1024)) != 0)) { msg.push("Einschränkungen"); }

            // Show user attributes
            var x = '<div style=min-height:80px><table style=width:100%>';
            if ((args.hide & 8) != 0) { x += '<br />' + addDeviceAttribute("Name", user.name); } // If title bar is hidden, display the user name here
            var email = user.email ? EscapeHtml(user.email) : '<i>' + "Nicht gesetzt" + '</i>', everify = '';
            var realname = user.realname ? EscapeHtml(user.realname) : '<i>' + "Nicht gesetzt" + '</i>';
            if (serverinfo.emailcheck) { everify = ((user.emailVerified == true) ? '<b style=color:green;cursor:pointer title="' + "E-Mail wird überprüft" + '">&#x2713</b> ' : '<b style=color:red;cursor:pointer title="' + "E-Mail nicht bestätigt" + '">&#x2717;</b> '); }

            if ((serverinfo.crossDomain) || (debugmode != 0)) {
                var d = user._id.split('/')[1];
                x += addDeviceAttribute("Domain", ((d != '') ? EscapeHtml(d) : ('<i>' + "Standard" + '</i>')));
                x += addDeviceAttribute("Benutzeridentifikation", EscapeHtml(user._id));
            } else {
                if (user.name.toLowerCase() != user._id.split('/')[2]) { x += addDeviceAttribute("Benutzeridentifikation", 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("E-Mail", '<span role=button onclick=p30showUserEmailChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + everify + email + emailLink + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
                x += addDeviceAttribute("Echter Name", '<span role=button onclick=p30showUserRealNameChangeDialog(event,"' + encodeURIComponentEx(user._id) + '")>' + realname + ' <i class="fa-solid fa-pencil fa-xs"></i></span>');
            } else {
                x += addDeviceAttribute("E-Mail", everify + email + emailLink);
                x += addDeviceAttribute("Echter Name", realname);
            }

            if ((features & 0x02000000) || (user.phone != null)) { // If SMS is enabled on the server or user has a phone number
                x += addDeviceAttribute("Telefonnummer", '<span role=button onclick=p30editPhone()>' + (user.phone ? user.phone : ('<i>' + "Kein(e)" + '</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("Benachrichtigung", '<span role=button onclick=p30editMessaging()><i class="fa-solid fa-comment fa-xs" title="' + "Benachrichtigungen aktiviert" + '" style="margin-top:2px"></i> ' + (user.msghandle ? user.msghandle : ('<i>' + "Kein(e)" + '</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("Sitzungen aufzeichnen"); }
            if (user.removeRights) {
                if ((user.removeRights & 0x00000008) != 0) { userFeatures.push("Keine Fernsteuerung"); } else {
                    if ((user.removeRights & 0x00010000) != 0) { userFeatures.push("Kein Desktop"); }
                    else if ((user.removeRights & 0x00000100) != 0) { userFeatures.push("Nur Desktop-Ansicht"); }
                    if ((user.removeRights & 0x00000200) != 0) { userFeatures.push("Kein Terminal"); }
                    if ((user.removeRights & 0x00000400) != 0) { userFeatures.push("Keine Dateien"); }
                }
                if ((user.removeRights & 0x00000010) != 0) { userFeatures.push("Keine Konsole"); }
                if ((user.removeRights & 0x00008000) != 0) { userFeatures.push("Keine Deinstallation"); }
                if ((user.removeRights & 0x00020000) != 0) { userFeatures.push("Kein Fernbefehl"); }
                if ((user.removeRights & 0x00000040) != 0) { userFeatures.push("Kein Aufwachen"); }
                if ((user.removeRights & 0x00040000) != 0) { userFeatures.push("Kein Zurücksetzen/Aus"); }
            }
            userFeatures = userFeatures.join(', ');
            if (userFeatures == '') { userFeatures = '<i>' + "Kein(e)" + '</i>'; }
            x += addDeviceAttribute("Eigenschaften", addLink(userFeatures, 'p20edituserfeatures()'));

            x += addDeviceAttribute("Server-Rechte", '<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("Server-Kontingent", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
            x += addDeviceAttribute("Erzeugung", printDateTime(new Date(user.creation * 1000)));
            if (user.login) x += addDeviceAttribute("Letzte Anmeldung", printDateTime(new Date(user.login * 1000)));
            if (user.passchange == -1) { x += addDeviceAttribute("Passwort", "Wird bei der nächsten Anmeldung geändert."); }
            else if (user.passchange) { x += addDeviceAttribute("Passwort", format("Letzte Änderung: {0}", printDateTime(new Date(user.passchange * 1000)))); }

            // Device Groups
            var linkCount = 0, linkCountStr = '<i>' + "Kein(e)" + '<i>';
            if (user.links) {
                for (var i in user.links) { if (i.startsWith('mesh/')) { linkCount++; } }
                if (linkCount == 1) { linkCountStr = "1 Gruppe"; } else if (linkCount > 1) { linkCountStr = format("{0} Gruppen", linkCount); }
            }
            x += addDeviceAttribute("Gerätegruppen", linkCountStr);

            // Administrative Realms
            if ((userinfo.siteadmin == 0xFFFFFFFF) || (userinfo.siteadmin & 2)) {
                var xuserGroups = '<i>' + "Kein(e)" + '</i>';
                if (user.groups) { xuserGroups = ''; for (var i in user.groups) { xuserGroups += '<span class="tagSpan">' + EscapeHtml(user.groups[i]) + '</span>'; } }
                x += addDeviceAttribute("Admin-Bereiche", 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("Desktop-Einverständnis & Datenschutzleiste"); } else if (consent & 0x0040) { meshFeatures.push("Desktop-Werkzeugleiste"); } else if (consent & 0x0008) { meshFeatures.push("Desktop-Einverständnis"); } else { if (consent & 0x0001) { meshFeatures.push("Desktop-Benachrichtigung"); } }
                if (consent & 0x0010) { meshFeatures.push("Terminal-Einverständnis"); } else { if (consent & 0x0002) { meshFeatures.push("Terminal-Benachrichtigung"); } }
                if (consent & 0x0020) { meshFeatures.push("Dateien-Einverständnis"); } else { if (consent & 0x0004) { meshFeatures.push("Dateien-Benachrichtigung"); } }
                if (consent == 7) { meshFeatures = ["Immer benachrichtigen"]; }
                if ((consent & 56) == 56) { meshFeatures = ["Immer nachfragen"]; }

                meshFeatures = meshFeatures.join(', ');
                if (meshFeatures == '') { meshFeatures = '<i>' + "Kein(e)" + '</i>'; }
                x += addDeviceAttribute("Benutzereinwilligung", 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("Authentifizierungsanwendung"); }
                if (user.otphkeys > 0) { factors.push("Sicherheitsschlüssel"); }
                if (user.otpekey > 0) { factors.push("E-Mail"); }
                if (user.otpduo > 0) { factors.push("Duo"); }
                if (user.otpkeys > 0) { factors.push("Sicherungscodes"); }
                if (user.otpdev > 0) { factors.push("Geräte-Push"); }
                if ((user.phone != null) && (features & 0x04000000)) { factors.push("SMS"); }
                if ((user.msghandle != null) && (features2 & 0x04000000)) { factors.push("Benachrichtigung"); }
                x += addDeviceAttribute("Sicherheit", '<i class="fa-solid fa-key" title="' + "Zweifaktorauthentifizierung eingeschaltet" + '" 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="' + "Notizen" + '" title="' + "Zeige Notizen zu diesem Benutzer" + '" onclick=showNotes(false,"' + encodeURIComponentEx(user._id) + '") />';
            if (user.phone && (features & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "SMS" + '" title="' + "Eine SMS an diesen Benutzer senden" + '" onclick=showSendSMS("' + encodeURIComponentEx(user._id) + '") />'; }
            if (user.msghandle && (features2 & 0x02000000)) { x += '<input type=button class="btn btn-primary btn-sm me-1" value="' + "Nachricht" + '" title="' + "Eine Nachricht an diesen Benutzer senden" + '" 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="' + "E-Mail" + '" title="' + "Eine E-Mail-Nachricht an diesen Benutzer senden" + '" 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="' + "Benachrichtigen" + '" title="' + "Sende Benutzerbenachrichtigung" + '" 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="' + "Entfernen Sie diesen Benutzer" + '">' + "Benutzer löschen" + '</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="' + "Ändern Sie das Passwort für diesen Benutzer" + '">' + "Passwort ändern" + '</a>';
                x += ' <a href=# style=cursor:pointer onclick=\'return p30viewPreviousLogins()\' title="' + "Vorherige Anmeldungen für diesen Benutzer anzeigen" + '">' + "Bisherige Logins" + '</a>';
            }
            x += '</div><br>'
            QH('p30html3', x);

            // Update user's connection state
            x = '';
            if (activeSessions == 1) { x = "1 aktive Sitzung"; } else if (activeSessions > 1) { x = format("{0} aktive Sitzungen", 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-fähige Telefonnummer für diesen Benutzer." + '<br />' + "Für nichts leer lassen.";
            x += '<br /><br /><div style=width:100%;text-align:center>' + "Telefonnummer:" + ' <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', "Telefonische Benachrichtigungen", 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%>' + "Benachrichtigungsaccount für diesen Benutzer.";
            var y = '<select id=d2serviceselect style=width:160px;margin-left:8px onchange=p30editMessagingValidate()><option value=0>' + "Kein(e)" + '</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>' + "Dienst" + '<td>' + y;
            x += '<tr><td>' + "Nutzername" + '<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">' + "Treten Sie dem Discordserver bei um Benachrichtigungen zu erhalten." + '</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">' + "Informationen bei 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'] = "Benutzername:0000"; }
            else if (Q('d2serviceselect').value == 8) { Q('d2handleinput')['placeholder'] = "benutzername@server.de"; }
            else if (Q('d2serviceselect').value == 16) { Q('d2handleinput')['placeholder'] = 'https://api.callmebot.com/...'; }
            else if (Q('d2serviceselect').value == 32) { Q('d2handleinput')['placeholder'] = "Nutzerschlüssel"; }
            else if (Q('d2serviceselect').value == 64) { Q('d2handleinput')['placeholder'] = "Thema"; }
            else if (Q('d2serviceselect').value == 128) { Q('d2handleinput')['placeholder'] = "benutzername@beispiel.de"; }
            else if (Q('d2serviceselect').value == 256) { Q('d2handleinput')['placeholder'] = 'https://hooks.slack.com/...'; }
            else { Q('d2handleinput')['placeholder'] = "Benutzername"; }
            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' : '') + '>' + "Sitzungen aufzeichnen" + '</label><br></div>';
            }
            x += '<div><label><input type=checkbox id=d20flag7 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000008) ? 'checked' : '') + '>' + "Keine Fernsteuerung" + '</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' : '') + '>' + "Kein Desktop-Zugriff" + '</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' : '') + '>' + "Nur entfernte Ansicht" + '</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' : '') + '>' + "Kein Terminalzugriff" + '</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' : '') + '>' + "Kein Dateizugriff" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag6 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000010) ? 'checked' : '') + '>' + "Keine Agent-Konsole" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag8 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00008000) ? 'checked' : '') + '>' + "Keine Deinstallation" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag9 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00020000) ? 'checked' : '') + '>' + "Kein Fernbefehl" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag10 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00000040) ? 'checked' : '') + '>' + "Kein Aufwachen" + '</label><br></div>';
            x += '<div><label><input type=checkbox id=d20flag11 class="form-check-input me-2" onchange=p20edituserfeaturesValidate() ' + ((removeRights & 0x00040000) ? 'checked' : '') + '>' + "Kein Zurücksetzen/Aus" + '</label><br></div>';

            setModalContent('xxAddAgent', "Benutzerfunktionen bearbeiten", 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("Ändern Sie den tatsächlichen Namen für {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("Status", '<select id=dp30verified class="form-control" onchange=p30validateEmail()><option value=0>' + "Nicht verifiziert" + '</option><option value=1>' + "Verifiziert" + '</option></select>'); }

            setModalContent('xxAddAgent', format("E-Mail für {0} ändern", 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', "Bisherige Logins", '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("Voraussetzungen: {0}.", r.join(', ')) + '</div>'; }
            }

            x += '<div><label><input id=p4resetNextLogin class="form-check-input me-2" type=checkbox />' + "Passwort beim nächsten Login zurücksetzen." + '</label></div>';
            if (multiFactor == 1) { x += '<div><label><input id=p4twoFactorRemove class="form-check-input me-2" type=checkbox />' + "Alle Zweifaktorauthentifizierung entfernen." + '</label></div>'; }
            setModalContent('xxAddAgent', format("Passwort für {0} ändern", 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("Benutzer {0} löschen", EscapeHtml(currentUser.name)), format("Löschen von Benutzer {0} bestätigen?", EscapeHtml(currentUser.name)) + '<br /><br /><label><input id=p10check type=checkbox class="form-check-input me-2" onchange=p10validateDeleteNodeDialog() />' + "Bestätigen" + '</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> ' + "Gerätegruppe hinzufügen" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Gemeinsame Gerätegruppen" + '</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>' + "Unbekannte Gerätegruppe" + '</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="' + "Entziehe Benutzerrechte an dieser Gerätegruppe" + '" 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="' + "Gerätegruppe" + '" 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>' + "Keine gemeinsamen Gerätegruppen" + '</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> ' + "Benutzergruppe hinzufügen" + '</button>'; }
                }
                x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Benutzergruppenmitgliedschaft" + '</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>' + "Unbekannte Benutzergruppe" + '</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="' + "Benutzergruppenmitgliedschaft entziehen" + '" 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="' + "Benutzergruppe" + '" 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>' + "Keine Benutzergruppenmitgliedschaften" + '</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> ' + "Gerät hinzufügen" + '</button>'; }
            x += '<table class="table table-hover"><tbody><tr class="table-active"><th scope=col>' + "Allgemeine Geräte" + '</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>' + "Unbekanntes Gerät" + '</i>');
                    if ((cr & 2) != 0) {
                        trash = '<a href=# onclick=\'return p30removeNodeFromUser(event,"' + encodeURIComponentEx(node._id) + '")\' title="' + "Entziehe Benutzerrechte an dieser Gerätegruppe" + '" 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="' + "Gerät" + '" 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>' + "Keine gemeinsamen Geräte" + '</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', "Entfernen Sie die Gerätefreigabe", format("Entfernen der Gerätefreigabe bestätigen \"{0}\"?", decodeURIComponent(guestname)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function (b, tag) { meshserver.send({ action: 'removeDeviceShare', nodeid: tag[0], publicid: tag[1] }); }, 3, [decodeURIComponent(nodeid), decodeURIComponent(publicid)]);
        }

        function p30removeNodeFromUser(event, nodeid) {
            if (xxdialogMode) return;
            var node = getNodeFromId(decodeURIComponent(nodeid));
            setModalContent('xxAddAgent', "Geräteberechtigungen entfernen", format("Bestätigen Sie den Entzug von Zugriffsrechten für Gerät \"{0}\"?", EscapeHtml(node.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function (b, node) { meshserver.send({ action: 'adddeviceuser', nodeid: node._id, nodename: node.name, userids: [currentUser._id], rights: 0, remove: true }); }, 3, node);
        }

        function p30removeUserFromNode(event, userid) {
            if (xxdialogMode) return;
            var user = null, name = '';
            userid = decodeURIComponent(userid);
            xxdialogButtons = 3; xxdialogTag = user;
            if (userid.startsWith('user/')) {
                if (users) { user = users[userid]; if (user != null) { name = user.name; } }
                setModalContent('xxAddAgent', "Benutzerberechtigungen entziehen", name ? format("Bestätigen Sie den Entzug von Zugriffsrechten für Benutzer \"{0}\"?", name) : "Bestätigen Sie den Entzug von Zugriffsrechten?");
                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', "Benutzergruppenberechtigungen entziehen", name ? format("Bestätigen Sie den Entzug von Zugriffsrechten für Benutzergruppe \"{0}\"?", name) : "Bestätigen Sie den Entzug von Zugriffsrechten?");
                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>' + "Unbekannt" + '</i>');
            setModalContent('xxAddAgent', "Benutzergruppenmitgliedschaft entziehen", format("Bestätigen Sie den Mitgliedschaftsentzug für Benutzergruppe \"{0}\"?", name));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p30RemoveUserGroupEx(3, groupid);
            });
        }

        function p30RemoveUserGroupEx(b, groupid) {
            meshserver.send({ action: 'removeuserfromusergroup', ugrpid: groupid, userid: currentUser._id });
        }

        function p30showAddUserGroupDialog() {
            if (xxdialogMode || (usergroups == null)) return;
            var y = '';
            for (var i in usergroups) {
                if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != currentUser._id.split('/')[1])) continue;
                if ((currentUser.links == null) || (currentUser.links[i] == null)) { y += '<option value=' + encodeURIComponentEx(i) + '>' + EscapeHtml(usergroups[i].name) + '</option>'; }
            }
            var x = addHtmlFormFloating("Benutzergruppe", '<select id=dp2groupid class="form-select">' + y + '</select>');
            xxdialogButtons = 3;
            setModalContent('xxAddAgent', "Mitgliedschaft hinzufügen", 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', "Gerätegruppenberechtigungen entfernen", format("Bestätigen Sie den Entzug von Zugriffsrechten für Gerätegruppe  \"{0}\"?", EscapeHtml(mesh.name)));
            showModal('xxAddAgentModal', 'idx_dlgOkButton', function () {
                p30removeMeshFromUserEx(3, mesh._id);
            });
        }

        function p30removeMeshFromUserEx(b, meshid) {
            meshserver.send({ action: 'removemeshuser', meshid: meshid, userid: currentUser._id });
        }


        //
        // UserDropDown Menu
        //

        var userDropdownOpen = false;
        var uiSubmenuOpen = false;

        function initializeUserDropdown() {
            var dropdownButton = Q('userDropdownButton');
            var dropdownMenu = Q('userDropdownMenu');
            var uiSettingsButton = document.querySelector('.userDropdownUISettings');
            var uiSubmenu = Q('uiSubmenu');

            if (!dropdownButton || !dropdownMenu) return;

            QS('uiSubmenu').display = 'none';
            QS('uiSubmenu').opacity = '0';
            QS('uiSubmenu').transform = 'translateX(12px)';
            dropdownMenu.classList.remove('show');

                dropdownButton.onclick = function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    userDropdownOpen = !userDropdownOpen;
                    dropdownMenu.classList.toggle('show');
                    if (!userDropdownOpen) {
                        QS('uiSubmenu').display = 'none';
                        QS('uiSubmenu').opacity = '0';
                        QS('uiSubmenu').transform = 'translateX(12px)';
                        uiSubmenuOpen = false;
                        resetChevronArrow();
                    }
                };

            document.addEventListener('click', function(e) {
                var uiSubmenu = Q('uiSubmenu');
                if (!dropdownMenu.contains(e.target) && !dropdownButton.contains(e.target) && !uiSubmenu.contains(e.target)) {
                    dropdownMenu.classList.remove('show');
                    QS('uiSubmenu').display = 'none';
                    QS('uiSubmenu').opacity = '0';
                    QS('uiSubmenu').transform = 'translateX(12px)';
                    userDropdownOpen = false;
                    uiSubmenuOpen = false;
                    resetChevronArrow();
                }
            });
        }

        function toggleUISubmenu() {
            uiSubmenuOpen = !uiSubmenuOpen;

            var uiSubmenu = Q('uiSubmenu');
            var isDesktop = window.innerWidth > 769;
            var reduceMotion = (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);

            if (uiSubmenuOpen) {
                QS('uiSubmenu').display = 'block';

                if (isDesktop && (reduceMotion == false)) {
                    setTimeout(function() {
                        QS('uiSubmenu').opacity = '1';
                        QS('uiSubmenu').transform = 'translateX(0)';
                    }, 10);
                } else {
                    QS('uiSubmenu').opacity = '1';
                    QS('uiSubmenu').transform = 'translateX(0)';
                }
            } else {
                if (isDesktop && (reduceMotion == false)) {
                    QS('uiSubmenu').opacity = '0';
                    QS('uiSubmenu').transform = 'translateX(12px)';
                    setTimeout(function() {
                        QS('uiSubmenu').display = 'none';
                    }, 300);
                } else {
                    QS('uiSubmenu').display = 'none';
                    QS('uiSubmenu').opacity = '0';
                    QS('uiSubmenu').transform = 'translateX(12px)';
                }
            }

            var chevronIcon = document.querySelector('.userDropdownUISettings .fa-chevron-right');
            if (chevronIcon) {
                chevronIcon.style.transform = uiSubmenuOpen ? 'rotate(90deg)' : '';
            }
        }

        function handleUserMenuItem(action) {
            Q('userDropdownMenu').classList.remove('show');
            QS('uiSubmenu').display = 'none';
            QS('uiSubmenu').opacity = '0';
            QS('uiSubmenu').transform = 'translateX(12px)';
            userDropdownOpen = false;
            uiSubmenuOpen = false;
            resetChevronArrow();

            switch(action) {
                case 'devices': goForward('devices'); break;
                case 'events': goForward('events'); break;
                case 'users': goForward('users'); break;
                case 'files': goForward('files'); break;
                case 'server': goForward('server'); break;
                case 'toggle-modern-ui': toggleBootstrapUIMode(); break;
                case 'left-bar': userInterfaceSelectMenu(1); break;
                case 'top-bar': userInterfaceSelectMenu(2); break;
                case 'fixed-width': userInterfaceSelectMenu(3); break;
                case 'toggle-footer': toggleFooterBarMode(); break;
                case 'toggle-night': toggleNightMode(); break;
                case 'notes': showNotes(false); break;
                case 'account': go(2); break;
                case 'logout':
                    if (logoutControls && logoutControls.logoutUrl) {
                        window.location.href = logoutControls.logoutUrl;
                    }
                    break;
            }
        }

        function resetChevronArrow() {
            var arrow = document.querySelector('.userDropdownUISettings .fa-chevron-right');
            if (arrow) arrow.style.transform = '';
        }

        function updateUserDropdownVisibility() {
            var userDropdown = Q('userDropdown');
            var dropdownName = Q('userDropdownName');
            var dropdownImage = Q('userDropdownImage');

            if (logoutControls && logoutControls.name) {
                QS('userDropdown').display = 'block';
                dropdownName.textContent = logoutControls.name;

                if (userinfo && userinfo.flags && (userinfo.flags & 1)) {
                    var rnd = userinfo.accountImageRnd || Math.floor(Math.random() * 9999999999);
                    dropdownImage.src = 'userimage.ashx?rnd=' + rnd;
                }
            } else {
                QS('userDropdown').display = 'none';
            }
        }

        function updateNightModeIcon() {
            var nightModeItem = document.querySelector('.userDropdownMenuItem[data-action="toggle-night"]');
            if (nightModeItem) {
                var icon = nightModeItem.querySelector('#nightModeIcon');
                var textSpan = nightModeItem.querySelector('#nightModeText');
                if (icon && textSpan) {
                    var isNightMode = document.body.classList.contains('night');
                    var iconClass = isNightMode ? 'fa-sun' : 'fa-moon';
                    var text = isNightMode ? "Lichtmodus umschalten" : "Schalten Sie den Dunkelmodus um";
                    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="' + "Dies ist eine Gast-Sharing-Sitzung" + '">' + 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="' + "Dies ist eine Gast-Sharing-Sitzung" + '">' + 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>' + "Keine Ereignisse vorhanden" + '</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>' + "Laden..." + '</i></div>';
            } else if (typeof p52recordings == 'number') {
                if (p52recordings == 1) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Der Server konnte keine Datei aus dem Aufzeichnungsordner laden." + '</i></div>'; }
                else if (p52recordings == 2) { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Der Server konnte keine Aufzeichnungen aus der Datenbank laden." + '</i></div>'; }
                else { x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Ein unbekannter Fehler ist aufgetreten." + '</i></div>'; }
            } else if (p52recordings.length == 0) {
                x += '<div style=width:100%;text-align:center;margin-top:20px><i>' + "Keine Aufnahmen." + '</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>' + "Sitzung" + '</th><th style=width:110px>' + nobreak("Startzeit") + '</th><th style=width:110px>' + "Dauer" + '</th><th style=width:110px>' + "Größe" + '</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>' + "Unbekannt" + '</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 += ' - ' + "Terminalsitzung"; }
            if (rec.protocol == 2) { sessionName += ' - ' + "Desktop-Sitzung"; }
            if (rec.protocol == 5) { sessionName += ' - ' + "Datei Übertragung"; }
            if (rec.protocol == 6) { sessionName += ' - ' + "Admin-PowerShell"; }
            if (rec.protocol == 8) { sessionName += ' - ' + "Benutzer-Shell"; }
            if (rec.protocol == 9) { sessionName += ' - ' + "Benutzer-PowerShell"; }
            if (rec.protocol == 100) { sessionName += ' - ' + "Intel&reg; AMT WSMAN"; }
            if (rec.protocol == 101) { sessionName += ' - ' + "Intel&reg; AMT Umleitung"; }
            if (rec.protocol == 200) { sessionName += ' - ' + "Messenger"; }

            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 = "Unbekannt";
                if (rec.protocol == 1) { protocolStr = "Terminal"; }
                if (rec.protocol == 2) { protocolStr = "Desktop"; }
                if (rec.protocol == 5) { protocolStr = "Dateien"; }
                if (rec.protocol == 6) { protocolStr = "Admin-PowerShell"; }
                if (rec.protocol == 8) { protocolStr = "Benutzer-Shell"; }
                if (rec.protocol == 9) { protocolStr = "Benutzer-PowerShell"; }
                if (rec.protocol == 100) { protocolStr = "Intel&reg; AMT WSMAN"; }
                if (rec.protocol == 101) { protocolStr = "Intel&reg; AMT Umleitung"; }
                if (rec.protocol == 200) { protocolStr = "Messenger"; }
                x += addHtmlValue4("Protokoll", protocolStr);
            }
            x += addHtmlValue4("Status", (rec.present == 1) ? "Auf dem Server vorhanden" : "Nicht auf dem Server");
            if (rec.name) { x += addHtmlValue4("Gerätename", EscapeHtml(rec.name)); }
            if (rec.meshname) { x += addHtmlValue4("Gerätegruppe", EscapeHtml(rec.meshname)); }
            if (rec.size) { x += addHtmlValue4("Größe", format("{0} Bytes", rec.size)); }
            if (rec.startTime) { x += addHtmlValue4("Startzeit", printTime(new Date(rec.startTime))); }
            if (rec.startTime && rec.lengthTime) { x += addHtmlValue4("Endzeit", printTime(new Date(rec.startTime + (rec.lengthTime * 1000)))); }
            if (rec.lengthTime) { x += addHtmlValue4("Dauer", 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("Multiplexer", "aktiviert"); }
            if (rec.userids) { for (var i in rec.userids) { x += addHtmlValue4("Benutzer", rec.userids[i].split('/')[2]); } }

            xxdialogButtons = 9;
            setModalContent('xxAddAgent', "Aufnahmedetails", 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: "Remote-Sitzungen", 2: "Benutzer Datendverkehrsaufkommen", 3: "Benutzer Anmeldungen" }
            if (userinfo.siteadmin == 0xFFFFFFFF) { options[4] = "Datenbankaufzeichnungen"; }
            for (var i in options) { y += '<option value=' + i + ((settings.type == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Modus", '<select id=d2reportType class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');

            x += '<div id=d2GroupByDiv style=display:none>';
            y = '';
            var options = { 1: "Benutzer", 2: "Gerät", 3: "Tag" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Gruppiere nach", '<select id=d2groupBy class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2GroupByDiv2 style=display:none>';
            y = '';
            var options = { 1: "Benutzer", 3: "Tag" }
            for (var i in options) { y += '<option value=' + i + ((settings.groupBy2 == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Gruppiere nach", '<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' : '') + '>' + "Alle" + '</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("Gerätegruppe", '<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: "Letzter Tag", 7: "Letzten 7 Tage", 30: "Letzte 30 Tage", 0: "Zeitspanne" }
            for (var i in options) { y += '<option value=' + i + ((settings.timeRange == i) ? ' selected' : '') + '>' + options[i] + '</option>'; }
            x += addHtmlFormFloating("Zeit", '<select id=d2timeRange class="form-select" onchange=generateReportDialogValidate()>' + y + '</select>');
            x += '</div>';

            x += '<div id=d2timeRangeDiv style=display:none>';
            x += addHtmlFormFloating("Zeitbereich Start", '<input id=d2timeRangeStartSelector class="flatpickr form-control" type="text" placeholder="' + "Datum & Uhrzeit auswählen..." + '" data-id="altinput">');
            x += addHtmlFormFloating("Zeitbereich Ende", '<input id=d2timeRangeEndSelector class="flatpickr form-control" type="text" placeholder="' + "Datum & Uhrzeit auswählen..." + '" 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' : '') + '>' + "Datenverkehr anzeigen" + '</label><div>');
            x += '</div>';

            setModalContent('xxAddAgent', "Bericht generieren", 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: "Zeit", device: "Gerät", session: "Sitzung", user: "Benutzer", devgroup: "Gerätegruppe", guest: "Gast", length: "Länge", bytesin: "Bytes ein", bytesout: "Bytes aus", os: "Betriebssystem", ip: "IP Adresse", browser: "Browser", msg: "Nachricht", twofactor: "2. Faktor" }
            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>' + "Der Bericht hat keine Einträge zurückgegeben." + '</tr></td>';
            } else {
                reportCSV = '"' + "Gruppe" + '"';
                x += '<tr>'
                for (var i in r.columns) {
                    var coltitle;
                    if (colTranslation[r.columns[i].title] != null) { coltitle = colTranslation[r.columns[i].title]; } else { coltitle = EscapeHtml(r.columns[i].title); }
                    var style = '';
                    if (r.columns[i].align) { style += ';text-align:' + EscapeHtml(r.columns[i].align); } else { style += ';text-align:left'; }
                    if ((i == 0) && ((r.columns[i].format == 'datetime') || (r.columns[i].format == 'time'))) { style += ';width:1%'; }
                    x += '<th style=' + style + '>' + coltitle + '</th>';
                    reportCSV += ',"' + csvClean(coltitle) + '"';
                    firstItem = false;
                }
                x += '</tr>'
                reportCSV += '\r\n';
                for (var i in r.groups) {
                    x += '<tr style=height:8px></tr>'
                    x += '<tr><td colspan=' + r.columns.length + ' style="border-bottom:1pt solid black"><b>'
                    if (i != 0) { x += renderReportFormat(i, r.groupFormat); }
                    x += '</b></td></tr>'
                    for (var j in r.groups[i].entries) {
                        var e = r.groups[i].entries[j];
                        x += '<tr>'
                        if (i != 0) { reportCSV += '"' + csvClean(renderReportFormatCSV(i, r.groupFormat)) + '"'; } else { reportCSV += '""'; }
                        for (var k in r.columns) {
                            var style = '';
                            if (r.columns[k].format != null) { style = 'white-space:nowrap;' }
                            if (r.columns[k].align) { style = 'text-align:' + EscapeHtml(r.columns[k].align); }
                            if (e[r.columns[k].id] != null) {
                                x += '<td style="' + style + '">' + renderReportFormat(e[r.columns[k].id], r.columns[k].format) + '</td>';
                                reportCSV += ',"' + csvClean(renderReportFormatCSV(e[r.columns[k].id], r.columns[k].format)) + '"';
                            } else {
                                x += '<td></td>';
                                reportCSV += ',';
                            }
                            if (r.columns[k].sumBy != null) {
                                var v1 = e[r.columns[k].sumBy];
                                if (r.columns[k].sumBy === true) { v1 = true; }
                                var v2 = e[r.columns[k].id];
                                sumByCol = r.columns[k].sumBy;
                                if (v2 != null) {
                                    if (sumByValues.indexOf(v1) == -1) { sumByValues.push(v1); }
                                    if (r.columns[k].subtotals == null) { r.columns[k].subtotals = {}; r.columns[k].totals = {}; }
                                    if (r.columns[k].subtotals[v1] == null) { r.columns[k].subtotals[v1] = v2; } else { r.columns[k].subtotals[v1] += v2; }
                                    if (r.columns[k].totals[v1] == null) { r.columns[k].totals[v1] = v2; } else { r.columns[k].totals[v1] += v2; }
                                }
                            }
                        }
                        x += '</tr>'
                        reportCSV += '\r\n';
                    }
                }

                // Display totals
                if (sumByCol != null) {
                    var sumByColNum = -1;
                    if (sumByCol === true) { sumByColNum = 99999; } else { for (var i in r.columns) { if (r.columns[i].id == sumByCol) { sumByColNum = i; } } }
                    if (sumByColNum >= 0) {
                        sumByValues.sort();
                        var firstRow = true;
                        x += '<tr style=height:8px></tr>'
                        for (var j in sumByValues) {
                            x += '<tr>'
                            for (var i in r.columns) {
                                if (i == sumByColNum) {
                                    var style = '';
                                    if (r.columns[k].align) { style += ';text-align:' + EscapeHtml(r.columns[k].align); }
                                    if (firstRow) { style += ';border-top:1pt solid #777'; }
                                    x += '<td style="color:#777' + style + '">' + renderReportFormat(sumByValues[j], r.columns[i].format); + '</td>';
                                } else if (r.columns[i].totals != null) {
                                    var style = '';
                                    if (r.columns[k].align) { style += ';text-align:' + EscapeHtml(r.columns[k].align); }
                                    if (firstRow) { style += ';border-top:1pt solid #777'; }
                                    x += '<td style="color:#777' + style + '">' + renderReportFormat(r.columns[i].totals[sumByValues[j]], r.columns[i].format); + '</td>';
                                } else {
                                    x += '<td></td>';
                                }
                            }
                            x += '</tr>'
                            firstRow = false;
                        }
                    }
                }
                QV('p60downloadReportDiv', true);
            }
            x += '</table>';
            QH('p60report', x);
        }

        function renderReportFormat(v, f) {
            if (v == null) return '';
            if (f == 'datetime') { return printDateTime(new Date(v)); }
            if (f == 'time') { return printTime(new Date(v)); }
            if (f == 'protocol') {
                if (v == 1) return "Terminal";
                if (v == 2) return "Desktop";
                if (v == 5) return "Dateien";
                if (v == 6) return "Admin-PowerShell";
                if (v == 8) return "Benutzer-Shell";
                if (v == 9) return "Benutzer-PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "Messenger";
                if (v == 201) return "Web-RDP";
                if (v == 202) return "Web-SSH";
                if (v == 203) return "Web-SFTP";
                if (v == 204) return "Web-VNC";
                return "Unbekannt" + ' (' + 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>' + "Unbekannt" + '</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>' + "Unbekannt" + '</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>' + "Unbekannt" + '</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>' + "Unbekannt" + '</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>' + "Benutzer unbekannt" + '</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>' + "Anmeldung erfolgreich";
                if (v == 108) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny2"></div>' + "Falscher 2. Faktor";
                if (v == 109) return '<div style=float:left;margin-right:4px;cursor:pointer class="NotifyIconTiny4"></div>' + "Gesperrtes Konto";
                if (v == 110) return '<div style=float:left;margin-right:4px;cursor:pointer><i class="fa-solid fa-exclamation-triangle text-warning"></i></div>' + "Ungültiger Anmeldeversuch";
            }
            if (f == '2fa') {
                if (v == '') { return "Kein(e)"; }
                if (v == 'backup') { return "Sicherungscodes"; }
                if (v == 'fido') { return "FIDO-Schlüssel"; }
                if (v == 'sms') { return "SMS Nachricht"; }
                if (v == 'hwotp') { return "Hardware-OTP"; }
                if (v == 'messenger') { return "Messenging"; }
                if (v == 'push') { return "Push Benachrichtigung"; }
                if (v == 'otp') { return "Einmalpasswort"; }
                if (v == 'cookie') { return "Gerät merken"; }
                if (v == 'tokenlogin') { return "Login-Token"; }
                if (v == 'ipaddr') { return "IP Adresse"; }
                if (v == 'sso') { return "Einmalige Anmeldung"; }
            }
            if (f == 'records') {
                var c = {
                    'node': "Geräteaufzeichnungen",
                    'mesh': "Gerätegruppenaufzeichnungen",
                    'user': "Benutzeraufzeichnungen",
                    'sysinfo': "Aufzeichnungen zu Geräteinformationen",
                    'iploc': "Aufzeichnungen zu IP Lokationsinformationen",
                    'note': "Aufzeichnungen zu Textnotizen",
                    'ifinfo': "Aufzeichnungen über Informationen zu Netzwerkschnittstellen",
                    'cfile': "Aufzeichnungen von Konfigurationsdatei",
                    'lastconnect': "Aufzeichnungen über letzte Verbindungszeiten",
                    'power': "Device power transition records",
                    'events': "Eventaufzeichnungen",
                    'serverstats': "Aufzeichnungen zu Serverstatistiken",
                    'ugrp': "Benutzergruppen-Log",
                    'deviceshare': "Aufzeichnungen zu Geräte-Teilen",
                    'logintoken': "Account login token records",
                    'smbios': "SMBIOS Geräteaufzeichnungen",
                    'pmt': "Aufzeichnungen zu Push-Benachrichtigungen des Geräts",
                    'meshcentral': "Aufzeichnungen zu Geräten, Benutzern und Weiteres"
                }
                return (c[v] != null) ? c[v] : v;
            }
            return EscapeHtml(v);
        }

        function renderReportFormatCSV(v, f) {
            if (f == 'datetime') { return printDateTime(new Date(v)); }
            if (f == 'time') { return printTime(new Date(v)); }
            if (f == 'protocol') {
                if (v == 1) return "Terminal";
                if (v == 2) return "Desktop";
                if (v == 5) return "Dateien";
                if (v == 6) return "Admin-PowerShell";
                if (v == 8) return "Benutzer-Shell";
                if (v == 9) return "Benutzer-PowerShell";
                if (v == 100) return "AMT-WSMAN";
                if (v == 101) return "AMT-Redir";
                if (v == 200) return "Messenger";
                if (v == 201) return "Web-RDP";
                if (v == 202) return "Web-SSH";
                if (v == 203) return "Web-SFTP";
                if (v == 204) return "Web-VNC";
                return "Unbekannt" + ' (' + 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 "Benutzer unbekannt";
                }
            }
            if (f == 'msg') {
                if (v == 107) return "Anmeldung erfolgreich";
                if (v == 108) return "Falscher 2. Faktor";
                if (v == 109) return "Gesperrtes Konto";
                if (v == 110) return "Ungültiger Anmeldeversuch";
            }
            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>' + "Derzeit gibt es keine Benachrichtigungen" + '</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("Aufgetreten am/um {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="' + "Löschen Sie diese Benachrichtigung" + '" 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">' + "Alles leeren" + '</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,
                "Neues Konto", // 1
                "Serverlimit",
                "Sicherheits Warnung",
                "Konto Einstellungen",
                "Gerätegruppe",
                "Codes einladen"
            ];
            var translatedMessages = [
                null,
                "Zugriff verweigert", // 1
                "Ungültiger Benutzername",
                "Ungültiges Passwort",
                "Ungültige E-Mail",
                "Ungültige Domain",
                "Ungültige Berechtigung der Seite",
                "Benutzer existiert bereits.",
                "In diesem Modus kann kein Benutzer hinzugefügt werden",
                "Validierungsausnahme",
                "Konten-Obergrenze erreicht.", // 10
                "Chat Anfrage, Drücken Sie hier zum annehmen.",
                "Seit der letzten Anmeldung sind {0} Anmeldeversuche für dieses Konto fehlgeschlagen.",
                "Fehler beim ändern der E-Mail-Adresse, diese wird bereits von einem anderen Konto verwendet: {0}.",
                "E-Mail gesendet.",
                "Benutzer {0} nicht gefunden",
                "Benutzer {0} nicht gefunden",
                "Fehler, Änderung zu einem zuvor verwendeten Passwort ist nicht möglich.",
                "Fehler, kann nicht zu häufig verwendeten Passwörtern geändert werden.",
                "Fehler, Passwort wurde nicht geändert.",
                "Passwort geändert.", // 20
                "Aktuelles Passwort ist nicht korrekt.",
                "Fehler, Einladungscode \"{0}\" wird bereits verwendet.",
                "SMS-Gateway nicht aktiviert",
                "Keine Benutzer Verwaltungsrechte",
                "Ungültige SMS Nachricht",
                "Keine Telefonnummer für diese Benutzer",
                "SMS erfolgreich gesendet.",
                "SMS Fehler",
                "SMS Fehler: {0}",
                "E-Mail domain \"{0}\" ist nicht erlaubt. Nur ({1}) ist erlaubt", // 30,
                "Ungültige Nachricht",
                "Nachricht erfolgreich gesendet.",
                "Fehler bei Nachricht",
                "Fehler bei Nachricht: {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: ["Verwendet", "Frei"] },
                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: ["Verwendet", "Frei"] },
                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: "Server-Zustand",
                AgentErrorCounters: "Agent-Fehlerzähler",
                UnknownGroup: "Unbekannte Gruppe",
                InvalidPKCSsignature: "Ungültige PKCS-Signatur",
                InvalidRSAsignature: "Ungültige RSA-Signatur",
                InvalidJSON: "Ungültiges JSON",
                UnknownAction: "Unbekannte Aktion",
                BadWebCertificate: "Ungültiges Web-Zertifikat",
                BadSignature: "Ungültige Signatur",
                MaxSessionsReached: "Maximale Sitzungen erreicht",
                UnknownDeviceGroup: "Unbekannte Gerätegruppe",
                InvalidDeviceGroupType: "Ungültiger Gerätegruppentyp",
                DuplicateAgent: "Agent-Duplikat",
                ConnectedIntelAMT: "Verbunden per Intel&reg; AMT",
                ConnectedIntelAMTCira: "Verbunden per Intel&reg; AMT CIRA",
                RelayErrors: "Weiterleitungsfehler",
                UserAccounts: "Benutzerkonten",
                DeviceGroups: "Gerätegruppen",
                AgentSessions: "Agent-Sitzungen",
                ConnectedUsers: "Verbundene Benutzer",
                UsersSessions: "Benutzersitzungen",
                RelaySessions: "Weiterleitungssitzungen",
                RelayCount: "Weiterleitungsanzahl"
            };

            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>' + "CPU-Auslastung" + '</div><div><b title="' + "CPU-Auslastung der vergangenen Minute" + '">' + (Math.round(message.cpuavg[0] * 100.0) / 100.0) + '</b>, <b title="' + "CPU-Auslastung der vergangenen 5 Minuten" + '">' + (Math.round(message.cpuavg[1] * 100.0) / 100.0) + '</b>, <b title="' + "CPU-Auslastung der vergangenen 15 Minuten" + '">' + (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>' + "Verfügbarer Speicher" + '</div><div><b>' + getNiceSize3(message.availablemem) + '</b> ' + "frei" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "gesamt" + '</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>' + "Speicher" + '</div><div><b>' + getNiceSize3(message.freemem) + '</b> ' + "frei" + ', <b>' + getNiceSize3(message.totalmem) + '</b> ' + "gesamt" + '</div>');
                QS('serverMemoryChartView')['display'] = 'inline-block';
                window.serverStatMemory.update();
            }

            // Display all of the server values
            var x = '<div style=width:100% cellpadding=0 cellspacing=0>';
            if (typeof message.values == 'object') {
                for (var i in message.values) {
                    x += '<div class=userTableHeader style=margin-bottom:4px;width:200px>' + (serverStatsStrings[i] ? serverStatsStrings[i] : i) + '</div>';
                    for (var j in message.values[i]) {
                        x += '<div style=display:inline-block class="me-2"><table class=serverStateTableCell><tr><td></td><td><span>' + (serverStatsStrings[j] ? serverStatsStrings[j] : j) + '</span><span style=float:right>' + message.values[i][j] + '</span></td><td></td></tr></table></div>';
                    }
                }
            }
            x += '</div>';

            QH('serverStatsTable', x);
        }

        //
        // MyServer Stats
        //

        var serverTimelineStats = null;
        var serverTimelineConfig = {
            type: 'line',
            data: { labels: [], datasets: [{ label: '', backgroundColor: 'rgba(255, 99, 132, .5)', borderColor: 'rgb(255, 99, 132)', data: [], fill: true }] },
            options: {
                animation: false,
                responsive: true,
                maintainAspectRatio: false,
                elements: { line: { cubicInterpolationMode: 'monotone' } },
                scales: {
                    x: { type: 'time', time: { tooltipFormat: 'll HH:mm' }, display: true, title: { display: false, text: '' } },
                    y: { type: 'linear', display: true, title: { display: true, text: '' }, stacked: true }
                }
            }
        };

        function refreshServerTimelineStats(stats) { if ((meshserver != null) && (meshserver.State == 2)) { meshserver.send({ action: 'servertimelinestats', hours: 24 * 30 }); } }
        function pastDate(hours) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); return t; }
        function setServerTimelineStats(stats) { serverTimelineStats = stats; updateServerTimelineStats(); }
        function addServerTimelineStats(stats) {
            if (serverTimelineStats == null) return;

            // Check if this new data is for our selected server
            var selectedServer = null;
            if (Q('p40server').value != null) {
                selectedServer = decodeURIComponent(Q('p40server').value);
                if (selectedServer == "") { selectedServer = null; }
            }
            if (stats.s != selectedServer) return;

            serverTimelineStats.push(stats);
            var chartType = Q('p40type').value;
            if (chartType == 0) { // Connections
                serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.conn.ca });
                serverTimelineConfig.data.datasets[1].data.push({ x: stats.time, y: stats.conn.cu });
                serverTimelineConfig.data.datasets[2].data.push({ x: stats.time, y: stats.conn.us });
                serverTimelineConfig.data.datasets[3].data.push({ x: stats.time, y: stats.conn.rs });
                serverTimelineConfig.data.datasets[4].data.push({ x: stats.time, y: stats.conn.am });
                if (stats.conn.amc != null) { serverTimelineConfig.data.datasets[5].data.push({ x: stats.time, y: stats.conn.amc }); }
            } else if (chartType == 1) { // Memory
                serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.mem.external / (1024 * 1024) });
                serverTimelineConfig.data.datasets[1].data.push({ x: stats.time, y: stats.mem.heapUsed / (1024 * 1024) });
                serverTimelineConfig.data.datasets[2].data.push({ x: stats.time, y: stats.mem.heapTotal / (1024 * 1024) });
                serverTimelineConfig.data.datasets[3].data.push({ x: stats.time, y: stats.mem.rss / (1024 * 1024) });
            } else if ((chartType == 3) || (chartType == 4)) { // Traffic
                updateServerTimelineStats();
            } else if (chartType == 5) { // CPU
                if ((typeof stats.cpu == 'object') && (typeof stats.cpu[0] == 'number')) { serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.cpu[0] }); }
            }
            /* else if (chartType == 2) {
                serverTimelineConfig.data.datasets[0].data.push({ x: stats.time, y: stats.db.meshes });
                serverTimelineConfig.data.datasets[1].data.push({ x: stats.time, y: stats.db.nodes });
                serverTimelineConfig.data.datasets[2].data.push({ x: stats.time, y: stats.db.users });
                serverTimelineConfig.data.datasets[3].data.push({ x: stats.time, y: stats.db.total });
            } */
            updateServerTimelineHours();
        }
        function updateServerTimelineHours() {
            serverTimelineConfig.options.scales.y.type = (Q('p40log').checked ? 'logarithmic' : 'linear');
            serverTimelineConfig.options.scales.x.min = pastDate(Q('p40time').value);
            window.serverMainStats.update();
        }

        function setupServerTimelineStats() { window.serverMainStats = new Chart(document.getElementById('serverMainStats').getContext('2d'), serverTimelineConfig); }
        const trafficSeriesNames = ["Agent", "CIRA", "AMT-OS", "HTTP", "Weiterleitung", "Terminal", "Desktop", "Dateien", "WebRDP", "WebSSH", "WebVNC", "Desktop-Multiplex"];
        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 = "Verbindungsanzahl";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Agent", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Benutzer", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Benutzersitzungen", data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: "Weiterleitungssitzungen", data: [], backgroundColor: 'rgba(39, 158, 16, .1)', borderColor: 'rgb(39, 158, 16)', fill: true },
                        { label: "Intel AMT", data: [], backgroundColor: 'rgba(134, 16, 158, .1)', borderColor: 'rgb(134, 16, 158)', fill: true },
                        { label: "Intel AMT CIRA", data: [], backgroundColor: 'rgba(255, 155, 0, .1)', borderColor: 'rgb(255, 155, 0)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    var t = new Date(serverTimelineStats[i].time);
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].first == true) {
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[1].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[2].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[3].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[4].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[5].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                    }
                    if (serverTimelineStats[i].conn) {
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.ca });
                        data.datasets[1].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.cu });
                        data.datasets[2].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.us });
                        data.datasets[3].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.rs });
                        data.datasets[4].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.am });
                        if (serverTimelineStats[i].conn.amc != null) { data.datasets[5].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].conn.amc }); }
                    }
                }
            } else if (chartType == 1) { // Memory
                serverTimelineConfig.options.scales.y.title.text = "Megabytes";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "Extern", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: "Heap verwendet", data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: "Heap gesamt", data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: "RSS", data: [], backgroundColor: 'rgba(39, 158, 16, .1)', borderColor: 'rgb(39, 158, 16)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].first == true) {
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[1].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[2].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        data.datasets[3].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                    }
                    data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.external / (1024 * 1024) });
                    data.datasets[1].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.heapUsed / (1024 * 1024) });
                    data.datasets[2].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.heapTotal / (1024 * 1024) });
                    data.datasets[3].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].mem.rss / (1024 * 1024) });
                }
            } else if ((chartType == 3) || (chartType == 4)) { // Inbound/Outbound traffic
                serverTimelineConfig.options.scales.y.title.text = "Megabytes";
                data = { labels: [pastDate(0), timeAfter], datasets: [] };
                var seriesWithData = 0;

                // See that series have non-zero values
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if (serverTimelineStats[i].traffic == null) continue;
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].traffic) {
                        if (serverTimelineStats[i].traffic.AgentCtrlIn > 0) { seriesWithData |= 0x0001; } // Agent control traffic
                        if (serverTimelineStats[i].traffic.CIRAIn > 0) { seriesWithData |= 0x0002; } // Intel AMT CIRA traffic
                        if (serverTimelineStats[i].traffic.LMSIn > 0) { seriesWithData |= 0x0004; } // Intel AMT LMS traffic
                        if (serverTimelineStats[i].traffic.httpIn > 0) { seriesWithData |= 0x0008; } // HTTP traffic
                        if (serverTimelineStats[i].traffic.relayIn) {
                            if (serverTimelineStats[i].traffic.relayIn[0] > 0) { seriesWithData |= 0x0010; } // Relay traffic = 0
                            if (serverTimelineStats[i].traffic.relayIn[1] > 0) { seriesWithData |= 0x0020; } // Terminal traffic = 1
                            if (serverTimelineStats[i].traffic.relayIn[2] > 0) { seriesWithData |= 0x0040; } // Desktop traffic = 2
                            if (serverTimelineStats[i].traffic.relayIn[5] > 0) { seriesWithData |= 0x0080; } // Files traffic = 5
                            if (serverTimelineStats[i].traffic.relayIn[10] > 0) { seriesWithData |= 0x0100; } // WebRDP traffic = 10
                            if (serverTimelineStats[i].traffic.relayIn[11] > 0) { seriesWithData |= 0x0200; } // WebSSH traffic = 11
                            if (serverTimelineStats[i].traffic.relayIn[12] > 0) { seriesWithData |= 0x0400; } // WebVNC traffic = 12
                        }
                        if (serverTimelineStats[i].traffic.desktopMultiplex) {
                            if (serverTimelineStats[i].traffic.desktopMultiplex.in > 0) { seriesWithData |= 0x0800; } // Desktop Multiplex traffic
                        }
                    }
                }

                // Setup data series labels
                for (var i = 0; i < 13; i++) { if (seriesWithData & (1 << i)) { data.datasets.push({ label: trafficSeriesNames[i], data: [], backgroundColor: seriesColor1(i), borderColor: seriesColor2(i), fill: true, steppedLine: true }); } }

                // Place all of the series data
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if (serverTimelineStats[i].traffic == null) continue;
                    if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                        servers.push(serverTimelineStats[i].s);
                        if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                    }
                    if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                    if (serverTimelineStats[i].s != selectedServer) { continue; }
                    if (serverTimelineStats[i].first == true) { for (var j = 1; j < 0xF000; j = j << 1) { if (seriesWithData & j) { data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN }); } } }
                    var z = 0;
                    if (chartType == 3) {
                        if (seriesWithData & 0x0001) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.AgentCtrlIn ? (serverTimelineStats[i].traffic.AgentCtrlIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0002) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.CIRAIn ? (serverTimelineStats[i].traffic.CIRAIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0004) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.LMSIn ? (serverTimelineStats[i].traffic.LMSIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0008) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.httpIn ? (serverTimelineStats[i].traffic.httpIn / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0010) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[0]) ? (serverTimelineStats[i].traffic.relayIn[0] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0020) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[1]) ? (serverTimelineStats[i].traffic.relayIn[1] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0040) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[2]) ? (serverTimelineStats[i].traffic.relayIn[2] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0080) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[5]) ? (serverTimelineStats[i].traffic.relayIn[5] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0100) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[10]) ? (serverTimelineStats[i].traffic.relayIn[10] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0200) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[11]) ? (serverTimelineStats[i].traffic.relayIn[11] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0400) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayIn && serverTimelineStats[i].traffic.relayIn[12]) ? (serverTimelineStats[i].traffic.relayIn[12] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0800) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.desktopMultiplex && serverTimelineStats[i].traffic.desktopMultiplex.in) ? (serverTimelineStats[i].traffic.desktopMultiplex['in'] / 0x100000) : 0 }); } // We have to put ['in'] here because the language translator will fail if using .in
                    } else if (chartType == 4) {
                        if (seriesWithData & 0x0001) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.AgentCtrlOut ? (serverTimelineStats[i].traffic.AgentCtrlOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0002) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.CIRAOut ? (serverTimelineStats[i].traffic.CIRAOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0004) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.LMSOut ? (serverTimelineStats[i].traffic.LMSOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0008) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].traffic.httpOut ? (serverTimelineStats[i].traffic.httpOut / (1024 * 1024)) : 0 }); }
                        if (seriesWithData & 0x0010) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[0]) ? (serverTimelineStats[i].traffic.relayOut[0] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0020) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[1]) ? (serverTimelineStats[i].traffic.relayOut[1] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0040) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[2]) ? (serverTimelineStats[i].traffic.relayOut[2] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0080) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[5]) ? (serverTimelineStats[i].traffic.relayOut[5] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0100) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[10]) ? (serverTimelineStats[i].traffic.relayOut[10] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0200) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[11]) ? (serverTimelineStats[i].traffic.relayOut[11] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0400) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.relayOut && serverTimelineStats[i].traffic.relayOut[12]) ? (serverTimelineStats[i].traffic.relayOut[12] / 0x100000) : 0 }); }
                        if (seriesWithData & 0x0800) { data.datasets[z++].data.push({ x: serverTimelineStats[i].time, y: (serverTimelineStats[i].traffic.desktopMultiplex && serverTimelineStats[i].traffic.desktopMultiplex.out) ? (serverTimelineStats[i].traffic.desktopMultiplex.out / 0x100000) : 0 }); }
                    }
                }
            } else if (chartType == 5) { // CPU
                serverTimelineConfig.options.scales.y.title.text = "Verwendungszweck";
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: "CPU", data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    if ((typeof serverTimelineStats[i].cpu == 'object') && (typeof serverTimelineStats[i].cpu[0] == 'number')) {
                        if ((serverTimelineStats[i].s != null) && (servers.indexOf(serverTimelineStats[i].s) == -1)) {
                            servers.push(serverTimelineStats[i].s);
                            if (serverAutoSelect) { selectedServer = serverTimelineStats[i].s; serverAutoSelect = false; }
                        }
                        if (serverTimelineStats[i].s == null) { serverEmptyExists = true; }
                        if (serverTimelineStats[i].s != selectedServer) { continue; }
                        if (serverTimelineStats[i].first == true) {
                            data.datasets[0].data.push({ x: serverTimelineStats[i].time - 1, y: NaN });
                        }
                        data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].cpu[0] });
                    }
                }
            }
            /*else if (chartType == 2) { // Database
                serverTimelineConfig.options.scales.y.title.text = 'Records';
                data = {
                    labels: [pastDate(0), timeAfter],
                    datasets: [
                        { label: 'Groups', data: [], backgroundColor: 'rgba(158, 151, 16, .1)', borderColor: 'rgb(158, 151, 16)', fill: true },
                        { label: 'Devices', data: [], backgroundColor: 'rgba(16, 84, 158, .1)', borderColor: 'rgb(16, 84, 158)', fill: true },
                        { label: 'Users', data: [], backgroundColor: 'rgba(255, 99, 132, .1)', borderColor: 'rgb(255, 99, 132)', fill: true },
                        { label: 'Records', data: [], backgroundColor: 'rgba(39, 158, 16, .1)', borderColor: 'rgb(39, 158, 16)', fill: true }
                    ]
                };
                for (var i = 0; i < serverTimelineStats.length; i++) {
                    data.datasets[0].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.meshes });
                    data.datasets[1].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.nodes });
                    data.datasets[2].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.users });
                    data.datasets[3].data.push({ x: serverTimelineStats[i].time, y: serverTimelineStats[i].db.total });
                }
            }*/
            serverTimelineConfig.data = data;
            window.serverMainStats.update();

            if (servers.length > 0) {
                var x = '';
                for (var i = 0; i < servers.length; i++) { x += '<option value="' + encodeURIComponentEx(servers[i]) + '"' + ((selectedServer == servers[i]) ? ' selected' : '') + '>' + EscapeHtml(servers[i]) + '</option>'; }
                if (serverEmptyExists) { x += '<option value=""' + ((selectedServer == null) ? ' selected' : '') + '>' + "Null" + '</option>'; }
                QH('p40server', x);
            }
            QV('p40server', servers.length > 0);
        }

        function p40downloadEvents() {
            var csv = "Zeit, conn.agent, conn.users, conn.usersessions, conn.relaysession, conn.intelamt, mem.external, mem.heapused, mem.heaptotal, mem.rss" + '\r\n';
            for (var i = 0; i < serverTimelineStats.length; i++) {
                if (serverTimelineStats[i].conn && serverTimelineStats[i].mem) {
                    csv += new Date(serverTimelineStats[i].time) + ', ' + serverTimelineStats[i].conn.ca + ', ' + serverTimelineStats[i].conn.cu + ', ' + serverTimelineStats[i].conn.us + ', ' + serverTimelineStats[i].conn.rs + ', ' + (serverTimelineStats[i].conn.am ? serverTimelineStats[i].conn.am : '') + ', ' + serverTimelineStats[i].mem.external + ', ' + serverTimelineStats[i].mem.heapUsed + ', ' + serverTimelineStats[i].mem.heapTotal + ', ' + serverTimelineStats[i].mem.rss + '\r\n';
                }
            }
            saveAs(stringToUtf8Blob(csv), 'ServerStats.csv');
        }

        //
        // My Server Tracing
        //

        var serverTrace = [];
        var serverTraceSources = [];

        function displayServerTrace() {
            var x = '', max = parseInt(Q('p41limitdropdown').value);
            if (serverTrace.length > max) { serverTrace.splice(max); }
            for (var i in serverTrace) {
                var args = [];
                for (var j in serverTrace[i].args) { if (typeof serverTrace[i].args[j] == 'object') { args.push(JSON.stringify(serverTrace[i].args[j])); } else { args.push(serverTrace[i].args[j]); } }
                x += '<div class=traceEvent onclick=showTraceEvent(' + i + ')>' + EscapeHtml(new Date(serverTrace[i].time).toLocaleTimeString()) + ' - <b>' + EscapeHtml(serverTrace[i].source.toUpperCase()) + '</b>: ' + EscapeHtml(args.join(', ')) + '</div>';
            }
            QH('p41events', x);
        }

        function showTraceEvent(i) {
            var e = serverTrace[i], x = '';
            if (xxdialogMode || (e == null)) return;
            for (var i in e.args) {
                if (typeof e.args[i] == 'object') { x += EscapeHtml(JSON.stringify(e.args[i])) + '<br /><br />'; } else { x += EscapeHtml(e.args[i]) + '<br /><br />'; }
            }
            xxdialogButtons = 1;
            setModalContent('xxAddAgent', "Server-Tracing-Ereignis", '<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>' + "Kernserver" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c1 ' + ((serverTraceSources.indexOf('cookie') >= 0) ? 'checked' : '') + '>' + "Cookie-Kodierer" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c2 ' + ((serverTraceSources.indexOf('dispatch') >= 0) ? 'checked' : '') + '>' + "Nachrichten-Verteiler" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c3 ' + ((serverTraceSources.indexOf('main') >= 0) ? 'checked' : '') + '>' + "Hauptserver-Nachrichten" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c4 ' + ((serverTraceSources.indexOf('peer') >= 0) ? 'checked' : '') + '>' + "MeshCentral-Serverpeering" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c15 ' + ((serverTraceSources.indexOf('agent') >= 0) ? 'checked' : '') + '>' + "Mesh-Agent-Datenverkehr" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c14 ' + ((serverTraceSources.indexOf('agentupdate') >= 0) ? 'checked' : '') + '>' + "Mesh-Agent-Aktualisierung" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c16 ' + ((serverTraceSources.indexOf('cert') >= 0) ? 'checked' : '') + '>' + "Server-Zertifikat" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c17 ' + ((serverTraceSources.indexOf('db') >= 0) ? 'checked' : '') + '>' + "Server-Datenbank" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c18 ' + ((serverTraceSources.indexOf('email') >= 0) ? 'checked' : '') + '>' + "E-Mail/SMS/Push-Verkehr" + '</label></div>';
            x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Webserver" + '</b></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c5 ' + ((serverTraceSources.indexOf('web') >= 0) ? 'checked' : '') + '>' + "Webserver" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c6 ' + ((serverTraceSources.indexOf('webrequest') >= 0) ? 'checked' : '') + '>' + "Webserver-Anfragen" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c7 ' + ((serverTraceSources.indexOf('relay') >= 0) ? 'checked' : '') + '>' + "Websocket-Weiterleitung" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c20 ' + ((serverTraceSources.indexOf('httpheaders') >= 0) ? 'checked' : '') + '>' + "HTTP-Header des Webservers" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c21 ' + ((serverTraceSources.indexOf('authlog') >= 0) ? 'checked' : '') + '>' + "Benutzerauthentifizierungs-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-Manager" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c9 ' + ((serverTraceSources.indexOf('webrelay') >= 0) ? 'checked' : '') + '>' + "Verbindungsweiterleitung" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c10 ' + ((serverTraceSources.indexOf('mps') >= 0) ? 'checked' : '') + '>' + "CIRA-Server" + '</label></div>';
            x += '<div><label><input type=checkbox class="form-check-input me-2" id=p41c11 ' + ((serverTraceSources.indexOf('mpscmd') >= 0) ? 'checked' : '') + '>' + "CIRA-Serverbefehle" + '</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', "Server-Tracing", 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 = "Zeit, Quelle, Nachricht" + '\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, "Konto-Sicherheit", 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', "Konto-Sicherheit", '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="' + "Version nicht kompatibel, bitte aktualisieren Sie zuerst Ihre MeshCentral-Installation" + '" 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="' + "Version nicht kompatibel, bitte aktualisieren Sie zuerst Ihre MeshCentral-Installation" + '" 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 = "Überprüfen...";
                    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="' + "Changelog anzeigen" + '" rel="noreferrer noopener" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
                        } else {
                            cant_action.push('upgrade');
                            if (p.status) p.upgradeAvail = "Aktuell";
                            else p.upgradeAvail = '<a title="' + "Changelog anzeigen" + '" 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', "Plugin-Aktion", format("Möchten Sie wirklich das Plugin {0}: {1}", elem.value, plugin.name) + '<input id="lastPluginAct" type="hidden" value="' + elem.value + '" /><input id="lastPluginId" type="hidden" value="' + id + '" /><input id="lastPluginVersion" type="hidden" value="" />');
                showModal('xxAddAgentModal', 'idx_dlgOkButton', () => pluginActionEx());
            }
            elem.value = '';
        }

        function goPlugin(pname, title) {
            if (pluginHandler == null) return;
            if (pname == null) { Q('p43iframe').src = ''; } else { QH('p43title', title); Q('p43iframe').src = '/pluginadmin.ashx?pin=' + pname; go(43); }
        }

        // Plugin Permissions Dialog - Custom Modal Implementation
        var currentPluginPermissions = null;
        var permissionListData = null;
        var permissionState = {}; // JavaScript state for permissions
        
        function openPluginPermissionsDialog(plugin) {
            meshserver.send({ action: 'getpluginpermissions', plugin: plugin.shortName });
            meshserver.send({ action: 'getpluginpermissionlist' });
            
            currentPluginPermissions = { plugin: plugin.shortName, name: plugin.name };
            
            // Create custom modal
            var modalHtml = createPluginPermissionsModal(plugin);
            document.body.insertAdjacentHTML('beforeend', modalHtml);
            
            // Show the custom modal
            var modal = Q('pluginPermModal');
            modal.style.display = 'block';
            document.body.classList.add('modal-open');
        }
        
        function createPluginPermissionsModal(plugin) {
            var x = '<div id="pluginPermModal" class="modal" style="display:none; position:fixed; z-index:10000; left:0; top:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5);">';
            x += '<div class="modal-dialog modal-xl" style="margin:30px auto; max-width:900px;">';
            x += '<div class="modal-content" style="background:#fff; border-radius:8px;">';
            
            // Header
            x += '<div class="modal-header" style="padding:15px 20px; border-bottom:1px solid #dee2e6; display:flex; justify-content:space-between; align-items:center;">';
            x += '<h5 class="modal-title" style="margin:0; font-size:18px;">' + "Plugin-Berechtigungen" + ' - ' + 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> ' + "Berechtigungen werden geladen" + '...</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;">' + "Berechtigungskaskade:" + ' Node → Mesh → Global → Default' + '</div>';
            x += '<div>';
            x += '<button type="button" class="btn btn-secondary" style="margin-right:10px;" onclick="closePluginPermModal()">' + "Abbrechen" + '</button>';
            x += '<button type="button" class="btn btn-primary" onclick="savePluginPermissionsEx()">' + "Speichern" + '</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">' + "Dieses Plugin hat keine Berechtigungen definiert." + '</div>';
                }
                return;
            }
            
            // Initialize permissionState from loaded data
            permissionState = {};
            window.meshOverridesState = {};
            window.nodeOverridesState = {};
            for (var permKey in definitions) {
                var permData = permissions[permKey] || { 
                    allowed: { users: [], userGroups: [], meshes: [], nodes: [] }, 
                    denied: { users: [], userGroups: [], meshes: [], nodes: [] },
                    meshOverrides: {}, 
                    nodeOverrides: {} 
                };
                permissionState[permKey] = {
                    allowed: { users: [], userGroups: [], meshes: [], nodes: [] },
                    denied: { users: [], userGroups: [], meshes: [], nodes: [] }
                };
                if (permData.allowed) {
                    if (permData.allowed.users) permissionState[permKey].allowed.users = permData.allowed.users.slice();
                    if (permData.allowed.userGroups) permissionState[permKey].allowed.userGroups = permData.allowed.userGroups.slice();
                    if (permData.allowed.meshes) permissionState[permKey].allowed.meshes = permData.allowed.meshes.slice();
                    if (permData.allowed.nodes) permissionState[permKey].allowed.nodes = permData.allowed.nodes.slice();
                }
                if (permData.denied) {
                    if (permData.denied.users) permissionState[permKey].denied.users = permData.denied.users.slice();
                    if (permData.denied.userGroups) permissionState[permKey].denied.userGroups = permData.denied.userGroups.slice();
                    if (permData.denied.meshes) permissionState[permKey].denied.meshes = permData.denied.meshes.slice();
                    if (permData.denied.nodes) permissionState[permKey].denied.nodes = permData.denied.nodes.slice();
                }
                
                // Initialize meshOverridesState and nodeOverridesState from loaded data
                window.meshOverridesState[permKey] = permData.meshOverrides || {};
                window.nodeOverridesState[permKey] = permData.nodeOverrides || {};
            }
            
            var x = '';
            
            // Custom tab buttons (no Bootstrap JS required)
            x += '<div class="mb-3" style="border-bottom:1px solid #dee2e6;">';
            x += '<button class="btn btn-sm btn-outline-primary me-1 active" id="btnTabGlobal" onclick="switchPermTab(\'global\')">' + "Global" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary me-1" id="btnTabMeshes" onclick="switchPermTab(\'meshes\')">' + "Gruppen" + '</button>';
            x += '<button class="btn btn-sm btn-outline-secondary" id="btnTabNodes" onclick="switchPermTab(\'nodes\')">' + "Knoten" + '</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' : '') + '>✓ ' + "Erlaubt" + '</option>';
                x += '<option value="denied"' + (defaultVal === 'denied' ? ' selected' : '') + '>✗ ' + "Verweigert" + '</option>';
                x += '<option value="inherited"' + (defaultVal === 'inherited' ? ' selected' : '') + '>↩ ' + "Geerbt" + '</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">' + "Erlaubt" + '</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">' + "Verweigert" + '</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="' + "Benutzer, Gruppen, Geräte suchen" + '..." ';
            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' ? "Gerätegruppe auswählen" : "Gerät auswählen" ) + ' --</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' ? "Spezifische Berechtigungen für diese Gerätegruppe" : "Spezifische Berechtigungen für dieses Gerät") + '</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("Aufruf-Fehler", 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>