<!DOCTYPE html><html lang="ko" dir="ltr" xmlns="http://www.w3.org/1999/xhtml"><head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <meta name="viewport" content="user-scalable=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0">
    <meta name="format-detection" content="telephone=no">
    <link rel="manifest" href="{{{domainurl}}}manifest.json">
    <link rel="shortcut icon" href="{{{domainurl}}}favicon.ico">
    <link rel="icon" type="image/png" sizes="16x16" href="{{{domainurl}}}favicon-16x16.png">
    <link rel="icon" type="image/png" sizes="32x32" href="{{{domainurl}}}favicon-32x32.png">
    <link rel="apple-touch-icon" href="/favicon-303x303.png">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="#ffffff">
    <meta name="apple-mobile-web-app-title" content="{{{title}}}">
    <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/agent-redir-ws-0.1.1{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/agent-desktop-0.0.2{{{min}}}.js"></script>
    <script type="text/javascript" src="scripts/amt-0.2.0{{{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-desktop-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 keeplink="1" type="text/javascript" src="scripts/filesaver.min.js"></script>
    <meta name="msapplication-TileColor" content="#00aba9">
    <meta name="theme-color" content="#ffffff">
    <title>{{{title}}}</title>
    <style>
        a {
            color: #036;
            text-decoration: underline;
        }

        #footer a {
            color: #fff;
            text-decoration: underline;
        }

            #footer a:hover {
                color: #fff;
                text-decoration: none;
            }

        .i1 {
            background: url(../images/icons50.png) 0px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .i2 {
            background: url(../images/icons50.png) -50px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .i3 {
            background: url(../images/icons50.png) -100px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .i4 {
            background: url(../images/icons50.png) -150px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .i5 {
            background: url(../images/icons50.png) -200px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .i6 {
            background: url(../images/icons50.png) -250px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .i7 {
            background: url(../images/icons50.png) -300px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .i8 {
            background: url(../images/icons50.png) -350px 0px;
            height: 50px;
            width: 50px;
            border: none;
        }

        .m0 {
            background: url(../images/images16.png) -32px 0px;
            height: 16px;
            width: 16px;
            border: none;
            float: left;
        }

        .m1 {
            background: url(../images/images16.png) -16px 0px;
            height: 16px;
            width: 16px;
            border: none;
            float: left;
        }

        .m2 {
            background: url(../images/images16.png) -96px 0px;
            height: 16px;
            width: 16px;
            border: none;
            float: left;
        }

        .m3 {
            background: url(../images/images16.png) -112px 0px;
            height: 16px;
            width: 16px;
            border: none;
            float: left;
        }

        .m4 {
            background: url(../images/images16.png) -128px 0px;
            height: 16px;
            width: 16px;
            border: none;
            float: left;
        }

        .NotifyIconSmall1 { width:24px; height:24px; background: url(../images/notify24.png) 0px 0px; }
        .NotifyIconSmall2 { width:24px; height:24px; background: url(../images/notify24.png) -24px 0px; }
        .NotifyIconSmall3 { width:24px; height:24px; background: url(../images/notify24.png) -48px 0px; }
        .NotifyIconSmall4 { width:24px; height:24px; background: url(../images/notify24.png) -72px 0px; }
        .NotifyIconSmall5 { width:24px; height:24px; background: url(../images/notify24.png) -96px 0px; }
        .NotifyIconSmall6 { width:24px; height:24px; background: url(../images/notify24.png) -120px 0px; }
        .NotifyIconSmall7 { width:24px; height:24px; background: url(../images/notify24.png) -144px 0px; }
        .NotifyIconSmall8 { width:24px; height:24px; background: url(../images/notify24.png) -168px 0px; }
        .NotifyIconSmall9 { width:24px; height:24px; background: url(../images/notify24.png) -192px 0px; }

        .gray {
            /*filter: url("data:image/svg+xml;utf8,&lt;svg xmlns=\'http://www.w3.org/2000/svg\'&gt;&lt;filter id=\'grayscale\'&gt;&lt;feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/&gt;&lt;/filter&gt;&lt;/svg&gt;#grayscale");*/ /* Firefox 10+, Firefox on Android */
            filter: gray; /* IE6-9 */
            -webkit-filter: grayscale(100%) opacity(60%); /* Chrome 19+, Safari 6+, Safari 6+ iOS */
        }

        .DevSt {
            padding-left: 5px;
            border-bottom-style: solid;
            border-bottom-width: 1px;
            border-bottom-color: #DDDDDD;
        }

        .noselect {
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            -khtml-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        .fileIcon1 {
            background: url(data:image/gif;base64,R0lGODlhEAAQAJEDAPb49Y2Sj9LT2f///yH5BAEAAAMALAAAAAAQABAAAAImnI+py+1vhJwyUYAzHTL4D3qdlJWaIFJqmKod607sDKIiDUP63hQAOw==);
            height: 16px;
            width: 16px;
            cursor: pointer;
            border: none;
            float: left;
            margin-top: 1px;
        }

        .fileIcon2 {
            background: url(data:image/gif;base64,R0lGODlhEAAQAJEDAM2xV/Xur+XPgP///yH5BAEAAAMALAAAAAAQABAAAAJD3ISZIGHWUGihznesYDYATFVM+D2hJ4lgN1olxALAtAlmPCJvuMmJd6PJckDYwicrHhTD5o7plJmg0Uc0asNMkphHAQA7);
            height: 16px;
            width: 16px;
            cursor: pointer;
            border: none;
            float: left;
            margin-top: 1px;
        }

        .fileIcon3 {
            background: url(data:image/gif;base64,R0lGODlhEAAQAJEDAPb19IGBgbq6uv///yH5BAEAAAMALAAAAAAQABAAAAIy3ISpxgcPH2ouQgFEw1YmxnUXKEaaEZZnVWZk66JwzKpvuwZzwOgwb/C1gIOA8Yg8DgoAOw==);
            height: 16px;
            width: 16px;
            cursor: pointer;
            border: none;
            float: left;
            margin-top: 1px;
        }

        .fileIcon4 {
            background: url(../images/meshicon16.png);
            height: 16px;
            width: 16px;
            cursor: pointer;
            border: none;
            float: left;
            margin-top: 1px;
        }

        .filelist {
            -moz-user-select: none;
            -khtml-user-select: none;
            -webkit-user-select: none;
            -o-user-select: none;
            cursor: default;
            -khtml-user-drag: element;
            background-color: white;
            clear: both;
        }

        .deviceNotifyDot {
            position:absolute;
            right:10px;
            top:0px;
            height:16px;
        }

        .deviceNotifyDotSub {
            text-align:center;
            color:#FFF;
            width:16px;
            background-color:#00F;
            padding:2px;
            border-radius:10px;
            box-shadow: 2px 2px 10px black;
            cursor:pointer;
            margin-left:3px;
            float:left;
        }

            .deviceNotifyDotSub:hover {
                background-color:#44F;
            }

        .deviceNotifySmallDot {
            position:absolute;
            right:10px;
            top:0px;
            height:10px;
        }

        .deviceNotifySmallDotSub {
            text-align:center;
            color:#FFF;
            width:10px;
            padding:2px;
            background-color:#00F;
            border-radius:10px;
            box-shadow: 2px 2px 10px black;
            cursor:pointer;
            margin-left:2px;
            float:left;
        }

        .deviceNotifySmallDotSub:hover {
            background-color:#44F;
        }

        .deviceNotifyLargeDot {
            position:absolute;
            right:10px;
            top:10px;
            height:40px;
        }

        .deviceNotifyLargeDotSub {
            text-align:center;
            width:35px;
            height:35px;
            color:#FFF;
            padding:2px;
            background-color:#00F;
            border-radius:20px;
            box-shadow: 2px 2px 10px black;
            cursor:pointer;
            margin-left:4px;
            font-size:30px;
            float:left;
        }

            .deviceNotifyLargeDotSub:hover {
                background-color:#44F;
            }

        .style10 {
            background-color: #C9C9C9;
            color: #000;
        }

        .deviceBatteryLarge {
            position:absolute;
            right:10px;
            top:0px;
            width:28px;
            height:48px;
            border:none;
            box-shadow:none;
        }

        .deviceBatteryLarge1 { background: url(../images/batteries48.png) 0px 0px; }
        .deviceBatteryLarge2 { background: url(../images/batteries48.png) -28px 0px; }
        .deviceBatteryLarge3 { background: url(../images/batteries48.png) -56px 0px; }
        .deviceBatteryLarge4 { background: url(../images/batteries48.png) -84px 0px; }
        .deviceBatteryLarge5 { background: url(../images/batteries48.png) -112px 0px; }
        .deviceBatteryLarge6 { background: url(../images/batteries48.png) -140px 0px; }
        .deviceBatteryLarge7 { background: url(../images/batteries48.png) -168px 0px; }
        .deviceBatteryLarge8 { background: url(../images/batteries48.png) -196px 0px; }
        .deviceBatteryLarge9 { background: url(../images/batteries48.png) -224px 0px; }
        .deviceBatteryLarge10 { background: url(../images/batteries48.png) -252px 0px; }
        .deviceBatteryLarge11 { background: url(../images/batteries48.png) -280px 0px; }

        .deviceBatterySmall {
            position:absolute;
            left:6px;
            top:22px;
            width:14px;
            height:24px;
            border:none;
            box-shadow:none;
        }

        .deviceBatterySmall1 { background: url(../images/batteries24.png) 0px 0px; }
        .deviceBatterySmall2 { background: url(../images/batteries24.png) -14px 0px; }
        .deviceBatterySmall3 { background: url(../images/batteries24.png) -28px 0px; }
        .deviceBatterySmall4 { background: url(../images/batteries24.png) -42px 0px; }
        .deviceBatterySmall5 { background: url(../images/batteries24.png) -56px 0px; }
        .deviceBatterySmall6 { background: url(../images/batteries24.png) -70px 0px; }
        .deviceBatterySmall7 { background: url(../images/batteries24.png) -84px 0px; }
        .deviceBatterySmall8 { background: url(../images/batteries24.png) -98px 0px; }
        .deviceBatterySmall9 { background: url(../images/batteries24.png) -112px 0px; }
        .deviceBatterySmall10 { background: url(../images/batteries24.png) -126px 0px; }
        .deviceBatterySmall11 { background: url(../images/batteries24.png) -140px 0px; }

        .devList1 {
            height: 50px;
            cursor: pointer;
            position: relative;
            margin-top: 5px;
            margin-bottom: 5px;
        }

        .devList2 {
            float: left;
            margin-left: 4px
        }

        .devList3 {
            width: auto;
            height: 40px;
            background-color: lightgray;
            margin-left: 60px;
            padding-top: 5px;
            padding-bottom: 5px;
            border-radius: 8px 0px 0px 8px;
        }

        .devList4 {
            padding-left: 12px;
            padding-top: 2px;
        }

        .devList5 {
            padding-left: 12px;
            padding-top: 3px;
            color: gray
        }
    </style>
</head>
<body onload="if (typeof(startup) !== 'undefined') startup();" style="overflow-y:hidden;margin:0;padding:0;border:0;color:black;font-size:13px;font-family:\'Trebuchet MS\', Arial, Helvetica, sans-serif">
    <div id="container">
        <div id="mastheadx"></div>
        <div id="masthead" style="background:url(logo.png) 0px 0px;background-size:341px 50px;background-color:#036;background-repeat:no-repeat;height:50px;width:100%;overflow:hidden">
            <div style="width:calc(100% - 50px);overflow:hidden">
                <div style="float:left;height:66px;color:#c8c8c8;padding-left:10px;padding-top:6px">
                    <strong><font style="font-size:36px;font-family:Arial,Helvetica,sans-serif">{{{title1}}}</font></strong>
                </div>
                <div style="float:left;height:66px;color:#c8c8c8;padding-left:5px;padding-top:10px">
                    <strong><font style="font-size:12px;font-family:Arial,Helvetica,sans-serif">{{{title2}}}</font></strong>
                </div>
            </div>
            <img id="topMenuIcon" class="noselect" style="position:absolute;right:0;top:10px;bottom:50px;color:#c8c8c8;font-size:44px;margin-right:8px;cursor:pointer;display:none" onclick="topMenu()" src="/images/3bars-30.png" width="30" height="30">
        </div>
        <div id="page_content" style="position:absolute;bottom:32px;top:50px;width:100%">
            <div id="column_l" style="width:100%;padding:0;position:absolute;bottom:0px;top:0px">
                <div id="p0" style="display:none;width:100%;height:100%">
                    <div style="display:flex;align-items:center;width:100%;height:100%">
                        <div id="p0message" style="text-align:center;width:100%"><span id="p0span">서버 연결이 끊어졌습니다</span>, <href onclick="reload()" style="cursor:pointer"><u>다시 연결하려면 클릭하십시오.</u></href>.</div>
                    </div>
                </div>
                <div id="p1" style="display:none;width:100%;height:100%">
                    <div style="display:flex;align-items:center;width:100%;height:100%">
                        <div id="p1message" style="text-align:center;width:100%"></div>
                    </div>
                </div>
                <div id="p2" style="display:none;position:absolute;top:0;left:0;right:0;bottom:0">
                    <div id="xdevices" style="position:absolute;overflow-y:auto;top:0;left:0;right:0;bottom:30px" onscroll="onDevicesScroll()" ontouchstart="onDeviceTouch(true)" ontouchend="onDeviceTouch(false)"></div>
                    <div id="xdevicesBar" style="position:absolute;overflow-y:auto;height:30px;left:0;right:0;bottom:0px;background-color:#aaa">
                        <div style="margin:4px">
                            <span style="width:20px;display:inline-block;text-align:center;cursor:pointer" onclick="clearSearchInput()"><b>X</b></span>
                            <input id="SearchInput" type="text" placeholder="필터" onchange="onDeviceSearchChanged(event)" onkeyup="onDeviceSearchChanged(event)" autocomplete="off">&nbsp;
                            <label class="noselect"><input type="checkbox" id="RealNameCheckBox" onclick="onRealNameCheckBox()">OS에서 설정한 이름</label>
                            <label class="noselect"><input type="checkbox" id="OnlineCheckBox" onclick="onOnlineCheckBox(event)">Online</label>
                        </div>
                    </div>
                </div>
                <div id="p3" style="display:none;position:absolute;bottom:0;top:0;width:100%">
                    <table cellspacing="0" style="margin:0;padding:0;border-spacing:0;border:0;">
                        <tbody><tr style="padding:0">
                            <td style="padding:0;color:#c8c8c8;text-align:center;cursor:pointer" width="60px" valign="top" onclick="goBack()">
                                <div style="padding:0;background-color:#036;width:10px;height:10px;float:right;border:0">
                                    <div style="background-color:white;width:10px;height:10px;border-radius:10px 0 0 0;border-right:1px solid white;border-bottom:1px solid white"></div>
                                </div>
                                <div style="padding:0;font-size:25px;background-color:#036;width:50px;border-radius:0 0 10px 0;height:36px">◀</div>
                            </td>
                            <td>
                                <img src="/images/user-50.png" width="50" height="50">
                            </td>
                            <td>
                                <div style="margin-left:5px">
                                    <strong style="font-size:large"><span id="p3userName"></span></strong><br>
                                </div>
                            </td>
                        </tr>
                    </tbody></table>
                    <div id="p3info" style="overflow-y:auto;position:absolute;top:55px;bottom:0px;width:100%">
                        <div style="margin-left:8px">
                            <div id="p3AccountActions">
                                <div id="p2AccountSecurity" style="display:none">
                                    <p><strong>계정 보안</strong></p>
                                    <div style="margin-left:9px;margin-bottom:8px">
                                        <div id="managePhoneNumber1" style="margin-top:5px;display:none"><a onclick="account_managePhone()" style="cursor:pointer">휴대폰 번호 관리</a> <span id="authPhoneNumberCheck"><strong>✓</strong></span></div>
                                        <div id="manageEmail2FA" style="margin-top:5px;display:none"><a onclick="account_manageAuthEmail()" style="cursor:pointer">이메일 인증 관리</a> <span id="authEmailSetupCheck"><strong>✓</strong></span></div>
                                        <div id="manageAuthApp" style="margin-top:5px;display:none"><a onclick="account_manageAuthApp()" style="cursor:pointer">2단계 인증앱 관리</a> <span id="authAppSetupCheck"><strong>✓</strong></span></div>
                                        <div id="manageOtp" style="margin-top:5px;display:none"><a onclick="account_manageOtp(0)" style="cursor:pointer">백업 코드 관리</a> <span id="authCodesSetupCheck"><strong>✓</strong></span></div>
                                    </div>
                                </div>
                                <div id="p2AccountActions" style="display:none">
                                    <p><strong>계정 동작</strong></p>
                                    <div style="margin-left:9px;margin-bottom:8px">
                                        <div style="margin-top:5px"><span id="managePhoneNumber2" style="display:none"><a onclick="account_managePhone()" style="cursor:pointer">휴대폰 번호 관리</a></span></div>
                                        <div style="margin-top:5px"><span id="verifyEmailId" style="display:none"><a onclick="account_showVerifyEmail()" style="cursor:pointer">이메일 검증</a></span></div>
                                        <div style="margin-top:5px"><span id="changeEmailId" style="display:none"><a onclick="account_showChangeEmail()" style="cursor:pointer">이메일 주소</a></span></div>
                                        <div style="margin-top:5px"><a onclick="account_showChangePassword()" style="cursor:pointer">비밀번호 변경</a><span id="p2nextPasswordUpdateTime"></span></div>
                                        <div style="margin-top:5px"><a onclick="account_showDeleteAccount()" style="cursor:pointer">계정 삭제</a></div>
                                    </div>
                                <br style="clear:both">
                                </div>
                            </div>
                            <strong>장치 그룹</strong>
                            <span id="p3createMeshLink1">( <a onclick="account_createMesh()" style="cursor:pointer"><img src="images/icon-addnew.png" width="12" height="12" border="0"> 새로운</a> )</span>
                            <br><br>
                            <div id="p3meshes"></div>
                            <div id="p3noMeshFound" style="margin-left:9px;display:none">장치 그룹이 없습니다.<span id="p3createMeshLink2"> <a onclick="account_createMesh()" style="cursor:pointer"><strong>여기서 시작하십시오!</strong></a></span></div>
                            <br style="clear:both">
                        </div>
                    </div>
                </div>
                <div id="p5" style="display:none">
                    <table cellspacing="0" style="margin:0;padding:0;border-spacing:0;border:0;">
                        <tbody><tr style="padding:0">
                            <td style="padding:0;color:#c8c8c8;text-align:center;cursor:pointer" width="60px" valign="top" onclick="goBack()">
                                <div style="padding:0;background-color:#036;width:10px;height:10px;float:right;border:0">
                                    <div style="background-color:white;width:10px;height:10px;border-radius:10px 0 0 0;border-right:1px solid white;border-bottom:1px solid white"></div>
                                </div>
                                <div style="padding:0;font-size:25px;background-color:#036;width:50px;border-radius:0 0 10px 0;height:36px">◀</div>
                            </td>
                            <td>
                                <img src="/images/user-50.png" width="50" height="50">
                            </td>
                            <td>
                                <div style="margin-left:5px">
                                    <strong style="font-size:large">내 파일</strong><br>
                                </div>
                            </td>
                        </tr>
                    </tbody></table>
                    <div id="p5myfiles" style="position:absolute;top:55px;bottom:0px;width:100%">
                        <table id="p5toolbar" style="width:100%;height:78px" cellpadding="0" cellspacing="0">
                            <tbody><tr>
                                <td style="width:100%;background-color:#d3d9d6;text-align:left;padding:4px" valign="bottom">
                                    <div style="width:100%;text-align:center">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5FolderUp" disabled="disabled" onclick="p5folderup()" value="상위 항목으로">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5SelectAllButton" disabled="disabled" onclick="p5selectallfile()" value="모두 선택" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5RenameFileButton" disabled="disabled" value="이름 바꾸기" onclick="p5renamefile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5DeleteFileButton" disabled="disabled" value="삭제" onclick="p5deletefile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5NewFolderButton" disabled="disabled" value="폴더" onclick="p5createfolder()" onkeypress="return false" onkeydown="return false">
                                    </div>
                                    <div style="width:100%;text-align:center">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5UploadButton" disabled="disabled" value="업로드" onclick="p5uploadFile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5CutButton" disabled="disabled" value="잘라내기" onclick="p5copyFile(1)" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5CopyButton" disabled="disabled" value="복사" onclick="p5copyFile(0)" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5PasteButton" disabled="disabled" value="붙여넣기" onclick="p5pasteFile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p5RefreshButton" value="새로 고침" onclick="p5refreshFiles()" onkeypress="return false" onkeydown="return false">
                                    </div>
                                </td>
                            </tr>
                            <tr>
                                <td style="background-color:#E4E9E7;height:28px">
                                    <table style="width:100%">
                                        <tbody><tr>
                                            <td id="p5currentpath" style="overflow:hidden;padding-left:4px;padding-top:2px"></td>
                                            <td style="text-align:right;padding-right:4px">
                                                <select id="p5sortdropdown" onchange="updateFiles()">
                                                    <option value="1" selected="selected">이름순으로 정렬</option>
                                                    <option value="2">크기별로 정렬</option>
                                                    <option value="3">날짜순으로 정렬</option>
                                                    <option value="4">이름 내림차순</option>
                                                    <option value="5">크기 내림차순</option>
                                                    <option value="6">날짜 내림차순</option>
                                                </select>
                                            </td>
                                        </tr>
                                    </tbody></table>
                                </td>
                            </tr>
                        </tbody></table>
                        <div id="p5filetable" style="width:100%;height:calc(100% - 102px);overflow:auto;-webkit-user-select:none">
                            <!--
                            <div id="p5bigok" style="width:256px;overflow:hidden;position:absolute;left:337px;top:200px;text-align:center;font-size:1600%;color:#AAAAAA;display:none"><b>&checkmark;</b></div>
                            <div id="p5bigfail" style="width:256px;overflow:hidden;position:absolute;left:337px;top:200px;text-align:center;font-size:1600%;color:#AAAAAA;display:none"><b>&#10007;</b></div>
                            -->
                            <span id="p5files"></span>
                        </div>
                        <table id="p5toolbarBottom" style="width:100%;height:22px;position:absolute;bottom:0px;background-color:#D3D9D6" cellpadding="0" cellspacing="0">
                            <tbody><tr>
                                <td style="text-align:left;padding:3px">&nbsp;<span id="p5bottomstatus"></span></td>
                                <td id="p5rightOfButtons" style="text-align:right;padding:3px"></td>
                            </tr>
                        </tbody></table>
                    </div>
                </div>
                <div id="p10" style="display:none;position:absolute;bottom:0;top:0;width:100%;overflow:hidden">
                    <table cellspacing="0" style="margin:0;padding:0;border-spacing:0;border:0;position:absolute;top:0">
                        <tbody><tr style="padding:0">
                            <td style="padding:0;color:#c8c8c8;text-align:center;cursor:pointer" width="60px" valign="top" onclick="goBack()">
                                <div style="padding:0;background-color:#036;width:10px;height:10px;float:right;border:0">
                                    <div style="background-color:white;width:10px;height:10px;border-radius:10px 0 0 0;border-right:1px solid white;border-bottom:1px solid white"></div>
                                </div>
                                <div style="padding:0;font-size:25px;background-color:#036;width:50px;border-radius:0 0 10px 0;height:36px">◀</div>
                            </td>
                            <td>
                                <a id="MainComputerImage" style="cursor:pointer" onclick="p10showiconselector()"></a>
                            </td>
                            <td>
                                <div style="margin-left:5px">
                                    <strong><span id="p10deviceName"></span></strong><br>
                                    <span id="MainComputerState"></span>
                                </div>
                            </td>
                        </tr>
                    </tbody></table>
                    <div id="p10general" style="overflow-y:scroll;position:absolute;top:55px;bottom:0px;width:100%">
                        <div class="deviceNotifyLargeDot">
                            <img id="p10deviceStar" class="deviceNotifyLargeDotSub" src="images/icon-star-notify-40.png" width="35" height="35">
                            <div id="p10deviceMsg" onclick="showDeviceMessages(null,null,event)" class="deviceNotifyLargeDotSub"></div>
                            <img id="p10deviceNotify" onclick="showDeviceSessions()" class="deviceNotifyLargeDotSub" src="images/icon-relay-notify-40.png" width="35" height="35">
                        </div>
                        <div id="p10deviceBattery" class="deviceBatteryLarge deviceBatteryLarge1"></div>
                        <div id="p10html" style="margin-left:8px;margin-right:8px"></div>
                        <div id="p10html2"></div>
                        <div id="p10html3"></div>
                    </div>
                    <div id="p10desktop" style="overflow:hidden;position:absolute;top:55px;bottom:0px;width:100%;display:none">
                        <div id="deskarea1" style="position:absolute;top:0px;width:100%;height:25px">
                            <div style="padding-top:2px;padding-bottom:2px;background:#C0C0C0">
                                <div style="float:right;text-align:right">
                                    <span id="p14power"></span>&nbsp;
                                    <input id="DeskSoftInput" type="text" style="width:25px;display:none;opacity:.2" onblur="toggleSoftKeys(0)" onkeypress="return ondeskkeypress(event)" onkeydown="return ondeskkeydown(event)" onkeyup="return ondeskkeyup(event)">
                                </div>
                                <div style="margin-left:3px">
                                    <input type="button" id="connectbutton1" value="연결" onclick="connectDesktop(event,1)" onkeypress="return false" onkeydown="return false" disabled="disabled">
                                    <input type="button" id="connectbutton1h" value="하드웨어 연결" onclick="connectDesktop(event,2)" onkeypress="return false" onkeydown="return false" disabled="disabled">
                                    <input type="button" id="disconnectbutton1" value="연결 해제" onclick="connectDesktop(event,0)" onkeypress="return false" onkeydown="return false">
                                    <span id="deskstatus">연결 해제</span>
                                </div>
                            </div>
                        </div>
                        <div id="deskarea3" style="position:absolute;top:25px;width:100%;height:calc(100% - 50px)">
                            <div id="deskarea3x" style="background:black;text-align:center;height:100%;position:relative">
                                <div id="DeskParent" style="height:100%">
                                    <canvas id="Desk" width="640" height="200" style="width:100%;-ms-touch-action:none;margin-left:0px" oncontextmenu="return false" onmousedown="dmousedown(event)" onmouseup="dmouseup(event)" onmousemove="dmousemove(event)" onmousewheel="dmousewheel(event)"></canvas>
                                </div>
                                <div id="DeskTools" style="position:absolute;width:400px;height:100%;background-color:gray;top:0;right:0;border-left:2px solid lightgray;display:none">
                                    <a id="DeskToolsRefreshButton" style="float:right;padding:3px;cursor:pointer" onclick="refreshDeskTools()">새로 고침</a>
                                    <div id="DeskToolsBar" style="position:absolute;padding:3px;border-radius: 3px 3px 0px 0px;top:5px;left:4px;bottom:26px;background-color:lightgray;cursor:pointer">프로세스</div>
                                    <div style="position:absolute;top:26px;left:4px;right:4px;bottom:4px;background-color:lightgray;text-align:left">
                                        <div style="border-bottom:1px solid darkgray;padding:3px"><a style="width:50px;padding-right:5px;float:left;cursor:pointer" onclick="sortProcess(0)">PID</a><a style="cursor:pointer" onclick="sortProcess(1)">이름</a></div>
                                        <div id="DeskToolsProcesses" style="overflow-y:scroll;position:absolute;top:24px;bottom:0px;width:100%"></div>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div id="deskarea4" style="position:absolute;bottom:0px;width:100%;height:25px">
                            <div style="padding-top:2px;padding-bottom:2px;background:#C0C0C0">
                                <div style="float:right;text-align:right">
                                    <span id="DeskToastButton"><img src="images/icon-notify.png" onclick="deviceToastFunction()" height="16" width="16" style="padding-top:2px"></span>&nbsp;
                                    <!--<input id=DeskToolsButton type=button value=Tools onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()">&nbsp;-->
                                </div>
                                <div>
                                    <input id="deskActionsBtn" type="button" style="margin-left:3px" onkeypress="return false" onkeydown="return false" value="동작" onclick="deviceActionFunction()">
                                    <input type="button" value="설정" onkeypress="return false" onkeydown="return false" onclick="showDesktopSettings()">
                                    <input type="button" onkeypress="return false" onkeydown="return false" value="전원 동작 ..." onclick="showPowerActionDlg()" style="display:none">
                                    <input type="button" id="DeskSpecialKeys" value="Keys" onkeypress="return false" onkeydown="return false" onclick="sendSpecialKeys()">
                                    <input type="button" id="DeskSoftKeys" value="키보드" onkeypress="return false" onkeydown="return false" onclick="toggleSoftKeys(1)">
                                    <input type="button" id="DeskScreens" value="Screens" onkeypress="return false" onkeydown="return false" onclick="deskSelectScreens()" style="display:none">
                                    <label><span id="DeskControlSpan" style="display:none"><input id="DeskControl" type="checkbox" onkeypress="return false" onkeydown="return false">입력</span></label>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div id="p10files" style="position:absolute;top:55px;bottom:0px;width:100%;display:none">
                        <table id="p13toolbar" style="width:100%;height:111px" cellpadding="0" cellspacing="0">
                            <tbody><tr>
                                <td style="background-color:#C0C0C0;border-bottom:2px solid black;padding:2px">
                                    <div style="float:right;text-align:right">
                                        <input id="filesActionsBtn" type="button" onkeypress="return false" onkeydown="return false" value="동작" onclick="deviceActionFunction()" style="margin-right:2px">
                                    </div>
                                    <div style="margin-left:2px">
                                        <input id="p13AutoConnect" value="자동 연결" onclick="autoConnectFiles(event)" onkeypress="return false" onkeydown="return false" type="button" style="display:none">
                                        <input id="p13Connect" value="연결" onclick="connectFiles(event)" onkeypress="return false" onkeydown="return false" type="button">
                                        <span id="p13Status">연결 해제</span>
                                    </div>
                                </td>
                            </tr>
                            <tr>
                                <td style="width:100%;background-color:#d3d9d6;text-align:left;padding:4px" valign="bottom">
                                    <div style="width:100%;text-align:center">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13FolderUp" disabled="disabled" onclick="p13folderup()" value="상위 항목으로">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13SelectAllButton" disabled="disabled" onclick="p13selectallfile()" value="모두 선택" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13RenameFileButton" disabled="disabled" value="이름 바꾸기" onclick="p13renamefile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13DeleteFileButton" disabled="disabled" value="삭제" onclick="p13deletefile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13NewFolderButton" disabled="disabled" value="폴더" onclick="p13createfolder()" onkeypress="return false" onkeydown="return false">
                                    </div>
                                    <div style="width:100%;text-align:center">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13UploadButton" disabled="disabled" value="업로드" onclick="p13uploadFile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13CutButton" disabled="disabled" value="잘라내기" onclick="p13copyFile(1)" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13CopyButton" disabled="disabled" value="복사" onclick="p13copyFile(0)" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13PasteButton" disabled="disabled" value="붙여넣기" onclick="p13pasteFile()" onkeypress="return false" onkeydown="return false">
                                        <input type="button" style="width:calc(100%/5 - 5px)" id="p13RefreshButton" disabled="disabled" value="새로 고침" onclick="p13folderup(9999)" onkeypress="return false" onkeydown="return false">
                                    </div>
                                </td>
                            </tr>
                            <tr>
                                <td style="background-color:#E4E9E7;height:28px">
                                    <table style="width:100%">
                                        <tbody><tr>
                                            <td id="p13currentpath" style="overflow:hidden;padding-left:4px;padding-top:2px"></td>
                                            <td style="text-align:right;padding-right:4px">
                                                <select id="p13sortdropdown" onchange="p13updateFiles()">
                                                    <option value="1" selected="selected">이름순으로 정렬</option>
                                                    <option value="2">크기별로 정렬</option>
                                                    <option value="3">날짜순으로 정렬</option>
                                                    <option value="4">이름 내림차순</option>
                                                    <option value="5">크기 내림차순</option>
                                                    <option value="6">날짜 내림차순</option>
                                                </select>
                                            </td>
                                        </tr>
                                    </tbody></table>
                                </td>
                            </tr>
                        </tbody></table>
                        <div id="p13filetable" style="width:100%;height:calc(100% - 133px);overflow:auto;-webkit-user-select:none">
                            <!--
                            <div id="p13bigok" style="width:256px;overflow:hidden;position:absolute;left:337px;top:200px;text-align:center;font-size:1600%;color:#AAAAAA;display:none"><b>&checkmark;</b></div>
                            <div id="p13bigfail" style="width:256px;overflow:hidden;position:absolute;left:337px;top:200px;text-align:center;font-size:1600%;color:#AAAAAA;display:none"><b>&#10007;</b></div>
                            -->
                            <span id="p13files"></span>
                        </div>
                        <table id="p13toolbarBottom" style="width:100%;height:22px;position:absolute;bottom:0px" cellpadding="0" cellspacing="0">
                            <tbody><tr><td style="text-align:left;padding:3px;text-align:center;background-color:#D3D9D6">&nbsp;<span id="p13bottomstatus"></span></td></tr>
                        </tbody></table>
                    </div>
                    <div id="p10details" style="overflow-y:scroll;position:absolute;top:55px;bottom:0px;width:100%">
                        <div id="p10detailshtml" style="margin-left:-3px"></div>
                    </div>
                </div>
                <div id="p20" style="display:none;position:absolute;bottom:0;top:0;width:100%">
                    <table cellspacing="0" style="margin:0;padding:0;border-spacing:0;border:0;position:absolute;top:0">
                        <tbody><tr style="padding:0">
                            <td style="padding:0;color:#c8c8c8;text-align:center;cursor:pointer" width="60px" valign="top" onclick="goBack()">
                                <div style="padding:0;background-color:#036;width:10px;height:10px;float:right;border:0">
                                    <div style="background-color:white;width:10px;height:10px;border-radius:10px 0 0 0;border-right:1px solid white;border-bottom:1px solid white"></div>
                                </div>
                                <div style="padding:0;font-size:25px;background-color:#036;width:50px;border-radius:0 0 10px 0;height:36px">◀</div>
                            </td>
                            <td onclick="p20editmesh(1)">
                                <img src="/images/meshicon50.png" width="50" height="50">
                            </td>
                            <td onclick="p20editmesh(1)">
                                <div style="margin-left:5px">
                                    <strong style="font-size:large"><span id="p20meshName"></span></strong><br>
                                </div>
                            </td>
                        </tr>
                    </tbody></table>
                    <div style="overflow-y:auto;position:absolute;top:55px;bottom:0px;left:0px;right:0px">
                        <div id="p20info" style="margin-left:8px;margin-right:8px"></div>
                    </div>
                </div>
            </div>
        </div>
        <div id="footer" style="height:32px;width:100%;text-align:center;background-color:#113962;position:absolute;bottom:0px">
            <table id="footerMenu" cellpadding="0" cellspacing="0" style="height:32px;width:100%;color:white;cursor:pointer;table-layout:fixed"></table>
        </div>
    </div>
    <div id="dialog" style="z-index:1000;background-color:#EEE;box-shadow:0px 0px 15px #666;font-family:Arial,Helvetica,sans-serif;border-radius:5px;position:fixed;top:90px;width:300px;display:none">
        <div style="width:100%;background-color:#003366;color:#FFF;border-radius:5px 5px 0 0">
            <div id="id_dialogclose" style="float:right;padding:5px;cursor:pointer" onclick="setDialogMode()"><b>X</b></div>
            <div id="id_dialogtitle" style="padding:5px"></div>
            <div style="width:100%;margin:6px"></div>
        </div>
        <div style="margin-right:16px;margin-left:8px">
            <div id="dialog1" style="margin:auto;text-align:center;margin:3px">
                <div id="id_dialogMessage" style="padding:10px"></div>
            </div>
            <div id="dialog2" style="margin:auto;margin:3px">
                <div id="id_dialogOptions"></div>
            </div>
            <div id="dialog3" style="margin:auto;margin:3px">
                <select id="deskkeys" style="width:100%">
                    <option value="10">Ctrl+Alt+Del</option>
                    <option value="11">Tab</option>
                    <option value="5">Win</option>
                    <option value="0">Win+아래</option>
                    <option value="1">Win+위</option>
                    <option value="2">Win+L</option>
                    <option value="3">Win+M</option>
                    <option value="4">Shift+Win+M</option>
                    <option value="6">Win+R</option>
                    <option value="7">Alt-F4</option>
                    <option value="8">Ctrl-W</option>
                    <option value="9">Alt-Tab</option>
                    <option value="12">Shift-F10</option>
                </select>
            </div>
            <div id="dialog7" style="margin:auto;margin:3px">
                <div id="d7meshkvm">
                    <h4 style="width:100%;border-bottom:1px solid gray">에이전트 원격 데스크톱</h4>
                    <div style="margin:3px 0 3px 0">
                        <select id="d7bitmapquality" style="float:right;width:200px;height:20px" dir="rtl"></select>
                        <div style="height:20px">품질</div>
                    </div>
                    <div style="margin:3px 0 3px 0">
                        <select id="d7bitmapscaling" style="float:right;width:200px;height:20px" dir="rtl">
                            <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>
                        <div style="height:20px">비율</div>
                    </div>
                    <div style="margin:3px 0 3px 0">
                        <select id="d7framelimiter" style="float:right;width:200px;height:20px" dir="rtl">
                            <option selected="selected" value="50">빠른</option>
                            <option value="100">중간</option>
                            <option value="400">느린</option>
                            <option value="1000">아주 느린</option>
                        </select>
                        <div style="height:20px">등급</div>
                    </div>
                </div>
                <div id="d7amtkvm">
                    <h4 style="width:100%;border-bottom:1px solid gray">Intel® AMT 하드웨어 KVM</h4>
                    <div style="height:26px">
                        <select id="d7desktopmode" style="float:right;width:200px">
                            <option value="1">RLE8, 가장 빠름</option>
                            <option value="2">RLE16, 권장</option>
                            <option value="3">RAW8, 느림</option>
                            <option value="4">RAW16, 매우 느림</option>
                        </select>
                        <div>인코딩</div>
                    </div>
                    <div style="height:60px">
                        <div style="float:right;border:1px solid #666;width:200px;height:60px;overflow-y:scroll;background-color:white">
                            <label><input type="checkbox" id="d7showfocus">초점 도구 표시</label><br>
                            <label><input type="checkbox" id="d7showcursor">로컬 마우스 커서 표시</label><br>
                        </div>
                        <div>다른</div>
                    </div>
                </div>
            </div>
        </div>
        <div id="idx_dlgButtonBar" style="padding:10px;margin-bottom:20px">
            <input id="idx_dlgCancelButton" type="button" value="취소" style="float:right;width:80px;margin-left:5px" onclick="dialogclose(0)">
            <input id="idx_dlgOkButton" type="button" value="확인" style="float:right;width:80px" onclick="dialogclose(1)">
        </div>
    </div>
    <div id="topMenu" style="z-index:1000;background-color:#EEE;box-shadow:0px 0px 15px #666;font-family:Arial,Helvetica,sans-serif;border-radius:0px 0px 5px 5px;position:fixed;top:50px;right:5px;width:170px;display:none">
        <div style="padding:12px;border-top:1px solid gray;color:black;cursor:pointer" onclick="topMenu(2)">내 파일</div>
        <div style="padding:12px;border-top:1px solid gray;color:black;cursor:pointer" onclick="topMenu(1)">내 계정</div>
        <div id="logoutMenuOption"><a href="/logout"><div style="padding:12px;border-top:1px solid gray;color:black;cursor:pointer">로그아웃</div></a></div>
    </div>
    <iframe name="fileUploadFrame" style="display:none"></iframe>
    <script>
        'use strict';

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

        var urlargs = parseUriArgs();
        if (urlargs.key && (isAlphaNumeric(urlargs.key) == false)) { delete urlargs.key; }
        if (urlargs.locale && (isAlphaNumeric(urlargs.locale) == false)) { delete urlargs.locale; }
        var args = urlargs;
        var debugLevel = parseInt('{{{debuglevel}}}');
        var features = parseInt('{{{features}}}');
        var sessionTime = parseInt('{{{sessiontime}}}');
        var sessionRefreshTimer = null;
        var domain = '{{{domain}}}';
        var domainUrl = '{{{domainurl}}}';
        var authCookie = '{{{authCookie}}}';
        var authRelayCookie = '{{{authRelayCookie}}}';
        var authCookieRenewTimer = null;
        var meshserver = null;
        var xdr = null;
        var usergroups = null;
        var stars = {}; // Devices that have been "stared" by the user.
        var serverinfo = null;
        var nodes = [];
        var meshes = {};
        var filetree = {};
        var userinfo = null;
        var serverinfo = null;
        var users = null;
        var nodeShortIdent = 0;
        var serverPublicNamePort = '{{{serverDnsName}}}:{{{serverPublicPort}}}';
        var debugmode = false;
        var attemptWebRTC = ((features & 128) != 0);
        var StatusStrs = ["연결 해제", "연결 중 ...", "설치...", "연결됨", "Intel&reg; AMT 연결"];
        var agentsStr = ["알 수 없는", "Windows 32비트 콘솔", "Windows 64비트 콘솔", "Windows 32비트 서비스", "Windows 64비트 서비스", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android ARM", "Linux ARM", "MacOS 32비트", "Android x86", "PogoPlug ARM", "Android APK", "Linux Poky x86-32비트", "MacOS 64비트", "ChromeOS", "Linux Poky x86-64비트", "Linux NoKVM x86-32비트", "Linux NoKVM x86-64비트", "Windows MinCore 콘솔", "Windows MinCore 서비스", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64비트", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "알 수 없는", "FreeBSD x86-64"];
        var files;
        var passRequirements = '{{{passRequirements}}}';
        if (passRequirements != '') { passRequirements = JSON.parse(decodeURIComponent(passRequirements)); }
        var sessionActivity = Date.now();

        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; }
            }

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

            window.onresize = center;
            center();
            QV('changeEmailId', (features & 0x200000) == 0);
            QH('p1message', "연결 중 ...");
            go(1);

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

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

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

            // Arrange the user interface
            QV('manageEmail2FA', features & 0x00800000);
            QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000));
            QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000));

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

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

        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();
        }

        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
                if (errorCode == 'noauth') { QH('p0span', "인증을 수행할 수 없습니다."); return; }
                if (prevState == 2) { setTimeout(serverPoll, 5000); } else { QH('p0span', "웹 소켓을 연결할 수 없습니다"); }
                // Clean up here
                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' });
                meshserver.send({ action: 'files' });
                if (xxcurrentView < 2) { go(2); }
                authCookieRenewTimer = setInterval(function () { meshserver.send({ action: 'authcookie' }); }, 1800000); // Request a cookie refresh every 30 minutes.
            }
            QV('topMenuIcon', state == 2);
        }

        // Poll the server, if it responds, refresh the page.
        function serverPoll() {
            xdr = null;
            try { xdr = new XDomainRequest(); } catch (e) { }
            if (!xdr) xdr = new XMLHttpRequest();
            xdr.open('HEAD', window.location.href);
            xdr.timeout = 15000;
            xdr.onload = function () { reload(); };
            xdr.onerror = xdr.ontimeout = function () { setTimeout(serverPoll, 10000); };
            xdr.send();
        }

        function updateSelf() {
            var accountSettingsLocked = false;
            if (userinfo) { accountSettingsLocked = ((userinfo.siteadmin != 0xFFFFFFFF) && ((userinfo.siteadmin & 1024) != 0)); }
            QV('p3AccountActions', ((features & 4) == 0) && (serverinfo.domainauth == false) && (accountSettingsLocked == false)); // Hide Account Actions if in single user mode or domain authentication
            QV('logoutMenuOption', ((features & 4) == 0) && (serverinfo.domainauth == false)); // Hide logout if in single user mode or domain authentication
            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('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
            QV('manageAuthApp', features & 4096);
            QV('manageOtp', ((features & 4096) != 0) && ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0)));
            QV('authPhoneNumberCheck', (userinfo.phone != null));
            QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
            QV('authAppSetupCheck', userinfo.otpsecret == 1);
            //QV('authKeySetupCheck', userinfo.otphkeys > 0);
            QV('authCodesSetupCheck', userinfo.otpkeys > 0);
            QV('p2AccountActions', ((features & 4) == 0) && (serverinfo.domainauth == false) && (userinfo != null) && (userinfo._id.split('/')[2].startsWith('~') == false));

            // On the mobile app, don't allow group creation (for now).
            QV('p3createMeshLink1', false);
            QV('p3createMeshLink2', false);

            if (typeof userinfo.passchange == 'number') {
                if (userinfo.passchange == -1) { QH('p2nextPasswordUpdateTime', " - 다음에 로그인할 때 초기화하십시오."); }
                else if ((passRequirements != null) && (typeof passRequirements.reset == 'number')) {
                    var seconds = (userinfo.passchange) + (passRequirements.reset * 86400) - Math.floor(Date.now() / 1000);
                    if (seconds < 0) { QH('p2nextPasswordUpdateTime', " - 다음에 로그인할 때 초기화하십시오."); }
                    else if (seconds < 3600) { var secs = Math.floor(seconds / 60); QH('p2nextPasswordUpdateTime',format((secs == 1)?" - 1분 후에 재설정하십시오.":" - {0}분 후에 재설정하십시오.", secs)); }
                    else if (seconds < 86400) { var hours = Math.floor(seconds / 3600); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - 1시간 후에 재설정합니다." : " - {0}시간 후에 재설정하십시오.", hours)); }
                    else { var days = Math.floor(seconds / 86400); QH('p2nextPasswordUpdateTime', format((hours == 1) ? " - 1일 후에 재설정합니다." : " - {0}일 후에 재설정하십시오.", days)); }
                }
            }
        }

        function setSessionActivity() { sessionActivity = Date.now(); }
        function checkIdleSessionTimeout() { var delta = (Date.now() - sessionActivity); if (delta > serverinfo.timeout) { window.location.href = 'logout'; } }

        function onMessage(server, message) {
            switch (message.action) {
                case 'serverinfo': {
                    serverinfo = message.serverinfo;
                    if (serverinfo.timeout) { setInterval(checkIdleSessionTimeout, 10000); checkIdleSessionTimeout(); }
                    if (userinfo != null) updateSelf();
                    break;
                }
                case 'authcookie': {
                    // Got an authentication cookie refresh
                    authCookie = message.cookie;
                    authRelayCookie = message.rcookie;
                    break;
                }
                case 'userinfo': {
                    userinfo = message.userinfo;
                    QH('p3userName', userinfo.name);
                    //updateSiteAdmin();
                    if (serverinfo != null) updateSelf();
                    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]; }
                    updateUsers();
                    break;
                }
                case 'wssessioncount': {
                    wssessions = message.wssessions;
                    updateUsers();
                    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]; }
                    updateMeshes();
                    updateDevices();
                    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++; }
                        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]) {
                            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;
                            message.nodes[m][n].desc = message.nodes[m][n].desc;
                            if (!message.nodes[m][n].icon) message.nodes[m][n].icon = 1;
                            message.nodes[m][n].ident = ++nodeShortIdent;
                            nodes.push(message.nodes[m][n]);
                        }
                    }

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

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

                    //onSortSelectChange();
                    //onSearchInputChanged();
                    updateDevices();
                    //refreshMap(false, true);
                    if (xxcurrentView == 0) { if ('{{viewmode}}' != '') { go(parseInt('{{viewmode}}')); } else { setDialogMode(0); go(2); } }
                    if ('{{currentNode}}' != '') { gotoDevice('{{currentNode}}', parseInt('{{viewmode}}')); }
                    break;
                }
                case 'powertimeline': {
                    if (message.nodeid != powerTimelineReq) break;
                    powerTimelineNode = message.nodeid;
                    powerTimeline = message.timeline;
                    powerTimelineUpdate = Date.now() + 300000; // Update every 5 minutes
                    if (currentNode._id == message.nodeid) { drawDeviceTimeline(); }
                    break;
                }
                case 'getsysinfo': {
                    if (message.nodeid != powerTimelineReq) break;
                    if (message.noinfo === true) {
                        updateDeviceDetails(getNodeFromId(message.nodeid));
                    } else {
                        updateDeviceDetails(getNodeFromId(message.nodeid), message.hardware);
                    }
                    break;
                }
                case 'getnetworkinfo': {
                    if (currentNode._id != message.nodeid) return;
                    updateDeviceDetails(getNodeFromId(message.nodeid), null, message);
                    break;
                }
                case 'otpauth-request': {
                    if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-request')) {
                        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', format("<a href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2\" rel=\"noreferrer noopener\" target=_blank>Google  Authenticator 설치</a> 또는 호환되는 응용 프로그램을 사용하려면 <a href=\"{0}\" rel=\"noreferrer noopener\" target=_blank>이 링크</a>를 사용하거나 아래의 암호화 코드을 입력하십시오. 그런 다음 현재 6자리 토큰을 입력하여 2단계 로그인을 활성화하십시오.", message.url) + '<br /><br /><div style=width:100%;text-align:center><tt id=d2optsecret secret="' + message.secret + '" style=font-size:15px>' + secret + '</tt><br /><br />Token: <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></div>');
                        QV('idx_dlgOkButton', true);
                        QE('idx_dlgOkButton', false);
                        Q('d2otpauthinput').focus();
                    }
                    break;
                }
                case 'otpauth-setup': {
                    if (xxdialogMode) return;
                    setDialogMode(2, "2단계 인증 앱", 1, null, message.success ? "<b style=color:green>2 단계 로그인 활성화 성공 </b>. 이제 다시 로그인하려면 유효한 토큰이 필요합니다." : "<b style=color:red>2 단계 로그인 활성화 실패 </b>. 응용 프로그램에서 비밀을 지우고 다시 시도하십시오. 올바른 코드를 입력하는 데 몇 분 밖에 걸리지 않습니다.");
                    break;
                }
                case 'otpauth-clear': {
                    if (xxdialogMode) return;
                    setDialogMode(2, "2단계 인증 앱", 1, null, message.success ? "<b style=color:green>2단계 로그인 활성화가 제거되었습니다 </b>. 언제든지 이 기능을 다시 활성화 할 수 있습니다." : "<b style=color:red>2 단계 로그인 활성화 제거에 실패했습니다 </b>. 다시 시도하십시오.");
                    break;
                }
                case 'otpauth-getpasswords': {
                    if (xxdialogMode) return;
                    var x = "일회성 토큰을 보조 인증으로 사용할 수 있습니다. 토큰들을 생성하고 인쇄하여 안전한 장소에 보관하십시오.";
                    x += '<div style=\'border-radius:6px;border: 2px dashed #888;width:100%;margin-top:8px\'><div style=\'padding:8px;font-family:Arial, Helvetica, sans-serif;font-size:20px;font-weight:bold\'><table style=width:100%;text-align:center>';
                    if (message.passwords) {
                        var j = 0;
                        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); } else { x += '<td><strike style=color:#BBB>' + p.substring(0, 4) + '&nbsp;' + p.substring(4); + '</strike>'; }
                        }
                    } else {
                        x += '<tr><td>' + "활성화된 토큰이 없습니다";
                    }
                    x += '</table></div></div><br />';
                    x += '<div><input type=button value=\'' + "닫기" + '\' onclick=setDialogMode(0) style=float:right></input>';
                    x += '<input type=button value=\'' + "새 토큰" + '\' onclick=\'account_manageOtp(1);\'></input>';
                    if (message.passwords != null) { x += '<input type=button value=\'' + "지우기" + '\' onclick=\'account_manageOtp(2);\'></input>'; }
                    x += '</div><br />';
                    setDialogMode(2, "백업 코드 관리", 8, null, x, 'otpauth-manage');
                    break;
                }
                case 'verifyPhone': {
                    if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return;
                    var x = '<table><tr><td><img src="images/phone80.png" style=padding:8px>';
                    x += '<td>Check your phone and enter the verification code.';
                    x += '<br /><br /><div style=width:100%;text-align:center>' + "검증 코드:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=6 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>';
                    setDialogMode(2, "휴대폰 알림", 3, account_managePhoneConfirm, x, message.cookie);
                    Q('d2phoneCodeInput').focus();
                    account_managePhoneCodeValidate();
                    break;
                }
                case 'event': {
                    /*
                    if (!message.event.nolog) {
                        events.unshift(message.event);
                        var eventLimit = parseInt(p3limitdropdown.value);
                        while (events.length > eventLimit) { events.pop(); } // Remove element(s) at the end
                        events_update();
                    }
                    */
                    if (message.event.noact) break; // Take no action on this event
                    switch (message.event.action) {
                        case 'userWebState': {
                            // New user web state, update the web page as needed
                            if (localStorage != null) {
                                var webstate = JSON.parse(message.event.state);
                                for (var i in webstate) { localStorage.setItem(i, webstate[i]); }

                                // Update stars
                                if (webstate.stars != null) { stars = JSON.parse(webstate.stars); }

                                // Update the web page
                                if ((webstate.loctag != null) && (webstate.loctag != oldLoctag)) {
                                    if (webstate.loctag != null) { args.locale = webstate.loctag; } else { delete args.locale; }
                                    updateDevices();
                                    updateMeshes();
                                } else if (webstate.stars != null) {
                                    updateDevices();
                                    if (Q('SearchInput').value == '*') { onSearchInputChanged(); }
                                }
                                if (currentNode) { refreshDevice(currentNode._id); }
                            }
                            break;
                        }
                        case 'accountchange': {
                            // An account was created or changed
                            if (userinfo.name == message.event.account.name) {
                                var newsiteadmin = message.event.account.siteadmin ? message.event.account.siteadmin : 0;
                                var oldsiteadmin = userinfo.siteadmin ? userinfo.siteadmin : 0;
                                if ((message.event.account.quota != userinfo.quota) || (((userinfo.siteadmin & 8) == 0) && ((message.event.account.siteadmin & 8) != 0))) { meshserver.send({ action: 'files' }); }
                                userinfo = message.event.account;
                                //if (oldsiteadmin != newsiteadmin) updateSiteAdmin();
                                updateSelf();

                                // If our list of nodes may have changes, request the new list now.
                                if (message.event.nodeListChange == userinfo._id) { meshserver.send({ action: 'nodes' }); }
                            }
                            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;
                                ugroup.desc = message.event.desc;
                                ugroup.links = message.event.links;
                            }
                            //mainUpdate(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' });
                            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) && ((userinfo.manageAllDeviceGroups) || (message.event.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] = { _id: message.event.meshid, name: message.event.name, mtype: message.event.mtype, desc: message.event.desc, links: message.event.links };
                                updateMeshes();
                                updateDevices();
                                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 };
                                    meshserver.send({ action: 'nodes' }); // Request a refresh of all nodes (TODO: We could optimize this to only request nodes for the new mesh).
                                }
                            } else {
                                // This is an existing mesh
                                if (meshes[message.event.meshid].name != message.event.name) {
                                    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(); } }
                                }
                                meshes[message.event.meshid].desc = message.event.desc;
                                meshes[message.event.meshid].links = message.event.links;

                                // 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 in the deleted mesh, move back to "My Devices"
                                    if (xxcurrentView >= 10 && xxcurrentView < 20 && currentNode && !IsNodeViewable(currentNode)) { setDialogMode(0); go(2); currentNode = null; }
                                }
                            }
                            updateMeshes();
                            updateDevices();
                            meshserver.send({ action: 'files' });

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

                            // Delete all nodes in that mesh
                            var newnodes = [];
                            for (var i in nodes) { if (nodes[i].meshid != message.event.meshid) { newnodes.push(nodes[i]); } }
                            nodes = newnodes;
                            updateDevices();

                            // 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(2); }

                            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;
                            nodes.push(node);
                            //onSortSelectChange();
                            //onSearchInputChanged();
                            updateDevices();
                            //updateMapMarkers();
                            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(2); }
                                    currentNode = null;
                                    // TODO: Correctly disconnect from this node (Desktop/Terminal/Files...)
                                }
                                nodes.splice(index, 1);
                                updateDevices();
                                //updateMapMarkers();
                            }
                            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.host = message.event.node.host;
                                node.desc = message.event.node.desc;
                                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.userloc = message.event.node.userloc;
                                node.rdpport = message.event.node.rdpport;
                                node.consent = message.event.node.consent;
                                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.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; }
                                    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; }
                                }
                                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; }

                                //onSortSelectChange(true);
                                //drawNotifications();
                                refreshDevice(node._id);
                                //updateMapMarkers();
                                updateDevices();

                                //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 (xxcurrentView >= 10 && xxcurrentView < 20 && currentNode && !IsNodeViewable(currentNode)) { setDialogMode(0); go(2); currentNode = null; }
                                    nodes.splice(index, 1);
                                } 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():'*';
                                }
                                updateDevices();
                                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);
                                updateDevices();
                            }
                            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];

                                // Change the node connection state
                                node.conn = message.event.conn;
                                node.pwr = message.event.pwr;

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

                                updateDevices();
                                refreshDevice(node._id);
                                //updateMapMarkers();
                            }
                            break;
                        }
                        case 'login': {
                            // Update the last login time
                            if (users != null && users['user/' + domain + '/' + message.event.username.toLowerCase()]) { users['user/' + domain + '/' + message.event.username.toLowerCase()].login = message.event.time; }
                            break;
                        }
                        case 'notify': {
                            //var n = { text: message.event.value };
                            //if (message.event.tag != null) { n.tag = message.event.tag; }
                            //addNotification(n);
                            break;
                        }
                        case 'sysinfohash': {
                            // If the sysinfo document has changed and we are looking at it, request an update.
                            if ((currentNode != null) && (message.event.nodeid == powerTimelineReq)) {
                                meshserver.send({ action: 'getsysinfo', nodeid: message.event.nodeid });
                            }
                            break;
                        }
                        case '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; }
                            }

                            updateDevices();
                            refreshDevice(message.event.nodeid);

                            //mainUpdate(4);
                            //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); }

                            break;
                        }
                        case 'stopped': { // Server is stopping.
                            // TODO: Disconnect
                            break;
                        }
                        default:
                            //console.log('Unknown message.event.action', message.event.action);
                            break;
                    }
                    break;
                }
                default:
                    //console.log('Unknown message.action', message.action);
                    break;
            }
        }

        //
        // Menu System
        //

        function topMenu(select) {
            if ((xxdialogMode != null) && (xxdialogMode != 0) && (xxdialogMode != 999)) return;
            if (select === undefined) {
                var x = (QS('topMenu').display == 'none');
                if (x == true) { if ((xxdialogMode == 0) || (xxdialogMode == null)) { QV('topMenu', true); xxdialogMode = 999; } } else { QV('topMenu', false); xxdialogMode = 0; }
            } else {
                QV('topMenu', false);
                xxdialogMode = 0;
                if ((select == 1) && (xxcurrentView != 3)) { goForward('account'); } // My Account
                if ((select == 2) && (xxcurrentView != 5)) { goForward('files'); } // My Files
            }
        }

        var backStack = [];
        function goBack() { if (xxdialogMode) return; if (backStack.length > 0) { backStack.pop(); } goStack(); }
        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 == 'account') { go(3); }
            if (idtype == 'devices') { go(2); }
            if (idtype == 'files') { go(5); }
        }

        function updateFooterMenu(options) {
            while (options != null && options.length < 3) { options.push({ n: '' }); }
            var x = '', prev = '';
            if (options != null) { for (var i in options) { x += '<td style="cursor:pointer' + ((prev == '') ? '' : ';border-left:solid 1px white') + '" onclick="' + options[i].f + '">' + options[i].n; prev = options[i].n; } }
            QH('footerMenu', '<tr>' + x);
        }

        //
        // MY ACCOUNT
        //

        function account_managePhone() {
            if (xxdialogMode || ((features & 0x02000000) == 0)) return;
            var x;
            if (userinfo.phone != null) {
                x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
                x += '<td style=text-align:center><div style=padding:6px>' + "검증된 휴대폰 번호" + '</div><div style=font-size:20px>' + userinfo.phone + '</div>';
                x += '<div style=margin:10px><label><input id=d2delPhone type=checkbox onclick=account_managePhoneRemoveValidate() />' + "휴대폰 번호 제거" + '</label></div>';
                setDialogMode(2, "휴대폰 알림", 3, account_managePhoneRemove, x);
                account_managePhoneRemoveValidate();
            } else {
                x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
                x += '<td>Enter your SMS capable phone number. Once verified, the number may be used for login verification and other notifications.';
                x += '<br /><br /><div style=width:100%;text-align:center>' + "휴대폰 번호:" + ' <input type=tel pattern="[0-9]" autocomplete="tel" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=account_managePhoneValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneValidate(1)"></div></table>';
                setDialogMode(2, "휴대폰 알림", 3, account_managePhoneAdd, x, 'verifyPhone');
                Q('d2phoneinput').focus();
                account_managePhoneValidate();
            }
        }

        function isPhoneNumber(x) { return x.match(/^\(?([0-9]{3,4})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) }
        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_manageAuthEmail() {
            if (xxdialogMode || ((features & 0x00800000) == 0)) return;
            var emailU2Fenabled = ((userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
            setDialogMode(2, "이메일 인증", 1, function () {
                if (emailU2Fenabled != Q('email2facheck').checked) { meshserver.send({ action: 'otpemail', enabled: Q('email2facheck').checked }); }
            }, "활성화되면, 보안 강화를 위해 각 로그인마다 이메일 계정으로 로그인 토큰을 받을 수 있는 옵션이 제공됩니다." + '<br /><br /><label><input id=email2facheck type=checkbox ' + (emailU2Fenabled ? 'checked' : '') + '/>' + "2단계 이메일 인증을 사용합니다." + '</label>');
        }

        function account_manageAuthApp() {
            if (xxdialogMode || ((features & 4096) == 0)) return;
            if (userinfo.otpsecret == 1) { account_removeOtp(); } else { account_addOtp(); }
        }

        function account_addOtp() {
            if (xxdialogMode || (userinfo.otpsecret == 1) || ((features & 4096) == 0)) return;
            setDialogMode(2, "2단계 인증 앱", 2, function () { meshserver.send({ action: 'otpauth-setup', secret: Q('d2optsecret').attributes.secret.value, token: Q('d2otpauthinput').value }); }, '<div id=d2optinfo>' + "불러오는 중 ..." + '</div>', 'otpauth-request');
            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;
            setDialogMode(2, "2단계 인증 앱", 3, function () { meshserver.send({ action: 'otpauth-clear' }); }, "2단계 인증 로그인 앱을 삭제하시겠습니까?");
        }

        function account_manageOtp(action) {
            if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-manage')) { dialogclose(0); }
            if (xxdialogMode || ((features & 4096) == 0) || ((userinfo.otpsecret != 1) && (userinfo.otphkeys < 1))) return;
            meshserver.send({ action: 'otpauth-getpasswords', subaction: action });
        }

        function account_showVerifyEmail() {
            if (xxdialogMode || (userinfo.emailVerified == true) || (serverinfo.emailcheck != true)) return;
            var x = "검증용 메일을 보내려면 OK을 클릭하십시오:" + '<br /><div style=padding:8px><b>' + EscapeHtml(userinfo.email) + '</b></div>' + "검증을 받으려면 몇 분 정도 기다려주십시오.";
            setDialogMode(2, "이메일 검증", 3, account_showVerifyEmailEx, x);
        }

        function account_showVerifyEmailEx() {
            meshserver.send({ action: 'verifyemail', email: userinfo.email });
        }

        function account_showChangeEmail() {
            if (xxdialogMode) return;
            var x = addHtmlValue("이메일", '<input id=dp3email style=width:170px maxlength=256 onchange=account_validateEmail() onkeyup=account_validateEmail(event) />');
            setDialogMode(2, "이메일 주소 변경", 3, account_changeEmail, x);
            if (userinfo.email != null) { Q('dp3email').value = userinfo.email; }
            account_validateEmail();
            Q('dp3email').focus();
        }

        function account_validateEmail(e, email) {
            QE('idx_dlgOkButton', validateEmail(Q('dp3email').value) && (Q('dp3email').value != userinfo.email));
            if ((e != null) && (e.keyCode == 13)) { dialogclose(1); }
        }

        function account_changeEmail() {
            meshserver.send({ action: 'changeemail', email: Q('dp3email').value });
        }

        function account_showDeleteAccount() {
            if (xxdialogMode) return;
            var x = '<form method=post><table style=margin-left:10px><input type=hidden name=action value=deleteaccount /><input type=hidden name=authcookie value=' + authCookie + ' /><tr>';
            x += '<td align=right>' + "암호:" + '</td><td><input id=apassword1 type=password name=apassword1 autocomplete=off onchange=account_validateDeleteAccount() onkeyup=account_validateDeleteAccount() /></td>';
            x += '</tr><tr><td align=right>' + "암호:" + '</td><td><input id=apassword2 type=password name=apassword2 autocomplete=off onchange=account_validateDeleteAccount() onkeyup=account_validateDeleteAccount() /></td>';
            x += '</tr></table><div style=padding:10px;margin-bottom:4px>';
            x += '<input id=account_dlgCancelButton type=button value="' + "취소" + '" style=float:right;width:80px;margin-left:5px onclick=dialogclose(0)>';
            x += '<input id=account_dlgOkButton type=submit value="' + "확인" + '" style="float:right;width:80px" onclick=dialogclose(1)>';
            x += '</div><br /></form>';
            setDialogMode(2, "계정 삭제", 0, null, x);
            account_validateDeleteAccount();
            Q('apassword1').focus();
        }


        function account_showChangePassword() {
            if (xxdialogMode) return false;
            var x = '<table style=margin-left:10px>';
            x += '<tr><td align=right>' + nobreak("이전 비밀번호:") + '</td><td><input id=apassword0 type=password name=apassword0 autocomplete=off onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword() /> <b></b></td></tr>';
            x += '<tr><td align=right>' + nobreak("새 비밀번호:") + '</td><td><input id=apassword1 type=password name=apassword1 autocomplete=off onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword() /> <b><span id=dxPassWarn></span></b></td></tr>';
            x += '<tr><td align=right>' + nobreak("새 비밀번호:") + '</td><td><input id=apassword2 type=password name=apassword2 autocomplete=off onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword() /></td></tr>';
            if (features & 0x00010000) { x += '<tr><td align=right>' + "비밀번호 힌트:" + '</td><td><input id=apasswordhint name=apasswordhint maxlength=250 type=text autocomplete=off onchange=account_validateNewPassword() onkeyup=account_validateNewPassword() onkeydown=account_validateNewPassword() /></td></tr>'; }
            x += '</table>'
            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>' + format("요구사항 : {0}.", r.join(', ')) + '</span>'; }
            }
            x += '<br />';
            setDialogMode(2, "비밀번호 변경", 3, account_showChangePasswordEx, x);
            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_createMesh() {
            if (xxdialogMode) return;

            // Check if we are disallowed from creating a device group
            if ((userinfo.siteadmin != 0xFFFFFFFF) && ((userinfo.siteadmin & 64) != 0)) { setDialogMode(2, "새 장치 그룹", 1, null, "이 계정에는 새 장치 그룹을 만들 수 있는 권한이 없습니다."); return; }

            // Remind the user to verify the email address
            if ((userinfo.emailVerified !== true) && (serverinfo.emailcheck == true) && (userinfo.siteadmin != 0xFFFFFFFF)) { setDialogMode(2, "계정 보안", 1, null, "이메일 주소가 확인 될 때까지 장치에 액세스 할 수 없습니다. 이것은 비밀번호 복구에 필요합니다. 이메일 주소를 변경하고 확인하려면 \"내 계정\"으로 이동하십시오."); return; }

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && !((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0) || (userinfo.otpkeys > 0) || ((features & 0x00800000) && (userinfo.otpekey == 1)))) { setDialogMode(2, "계정 보안", 1, null, "2단계 인증이 활성화 될 때까지 장치에 액세스 할 수 없습니다. 이것은 추가 보안을 위해 필요합니다. \"내 계정\" 탭으로 이동하여 \"계정 보안\" 부분을 보십시오."); return; }

            // We are allowed, let's prompt to information
            var x = addHtmlValue("이름", '<input id=dp3meshname style=width:170px maxlength=64 onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate() />');
            x += addHtmlValue("유형", '<div style=width:170px;margin:0;padding:0><select id=dp3meshtype style=width:100% onchange=account_validateMeshCreate() ><option value=2>' + "소프트웨어 에이전트 그룹" + '</option><option value=1>' + "오직 Intel&reg; AMT만" + '</option></select></div>');
            x += addHtmlValue("설명", '<div style=width:170px;margin:0;padding:0><textarea id=dp3meshdesc maxlength=1024 style=width:100%;resize:none></textarea></div>');
            setDialogMode(2, "장치 그룹 만들기", 3, account_createMeshEx, x);
            account_validateMeshCreate();
            Q('dp3meshname').focus();
        }

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

        function account_createMeshEx(button, tag) {
            meshserver.send({ action: 'createmesh', meshname: Q('dp3meshname').value, meshtype: Q('dp3meshtype').value, desc: Q('dp3meshdesc').value });
        }

        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>Strong<span>'; } else if (passStrength >= 60) { r = '<span style=color:blue>&#9679;<span>'; } else { r = '<span style=color:red>&#9679;<span>'; }
                } else {
                    // Password requirements provided, use that
                    var passReq = checkPasswordRequirements(Q('apassword1').value, passRequirements);
                    if (passReq == false) { ok = false; r = '<span style=color:red>' + "정책" + '<span>' }
                }
            }
            QH('dxPassWarn', r);
            //QE('account_dlgOkButton', ok);
            QE('idx_dlgOkButton', ok);
        }

        // Return a password strength score
        function checkPasswordStrength(password) {
            var r = 0, letters = {}, varCount = 0, variations = { digits: /\d/.test(password), lower: /[a-z]/.test(password), upper: /[A-Z]/.test(password), nonWords: /\W/.test(password) }
            if (!password) return 0;
            for (var i = 0; i < password.length; i++) { letters[password[i]] = (letters[password[i]] || 0) + 1; r += 5.0 / letters[password[i]]; }
            for (var c in variations) { varCount += (variations[c] == true) ? 1 : 0; }
            return parseInt(r + (varCount - 1) * 10);
        }

        // Check password requirements
        function checkPasswordRequirements(password, requirements) {
            if ((requirements == null) || (requirements == '') || (typeof requirements != 'object')) return true;
            if (requirements.min) { if (password.length < requirements.min) return false; }
            if (requirements.max) { if (password.length > requirements.max) return false; }
            var numeric = 0, lower = 0, upper = 0, nonalpha = 0;
            for (var i = 0; i < password.length; i++) {
                if (/\d/.test(password[i])) { numeric++; }
                if (/[a-z]/.test(password[i])) { lower++; }
                if (/[A-Z]/.test(password[i])) { upper++; }
                if (/\W/.test(password[i])) { nonalpha++; }
            }
            if (requirements.numeric && (numeric < requirements.numeric)) return false;
            if (requirements.lower && (lower < requirements.lower)) return false;
            if (requirements.upper && (upper < requirements.upper)) return false;
            if (requirements.nonalpha && (nonalpha < requirements.nonalpha)) return false;
            return true;
        }

        function updateMeshes() {
            var r = '', count = 0;
            for (i in meshes) {
                count++;

                // Mesh rights
                var meshrights = GetMeshRights(meshes[i]);
                var rights = "부분적인 권리";
                if (meshrights == 0xFFFFFFFF) rights = "전체 관리자"; else if (meshrights == 0) rights = "권한이 없습니다.";

                // Print the mesh information
                r += '<div style=cursor:pointer onclick=goForward(\'' + i + '\')>';
                r += '<div style="float:left;margin-left:4px"><img src="/images/meshicon50.png" width=50 height=50 /></div>';
                r += '<div style="width:auto;height:40px;background-color:lightgray;margin-top:5px;margin-bottom:5px;margin-left:60px;padding-top:5px;padding-bottom:5px;border-radius:8px 0px 0px 8px">';
                r += '<div><div style=padding-left:12px;padding-top:2px><b>' + EscapeHtml(meshes[i].name) + '</b></div><div style=padding-left:12px;padding-top:3px;color:gray>' + rights + '</div></div>';
                r += '</div></div>';
            }

            QH('p3meshes', r);
            QV('p3noMeshFound', count == 0);
        }

        function gotoMesh(meshid) {
            currentMesh = meshes[meshid];
            if (currentMesh == null) { goBack(); }
            p20updateMesh();
            go(20);
        }

        //
        // MY FILES
        //

        var filetreelinkpath;
        var filetreelocation = [];

        function p5refreshFiles() { meshserver.send({ action: 'files' }); }

        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 style=cursor:pointer onclick=p5folderup(0)>' + "최상위폴더" + '</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 + 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 style=cursor:pointer onclick=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;
                shortname = name;
                if (name.length > 40) { shortname = EscapeHtml(name.substring(0, 40)) + "..."; } else { shortname = EscapeHtml(name); }
                name = EscapeHtml(name);

                // Figure out the date
                //var fdatestr = '';
                //if (f.d != null) { var fdate = new Date(f.d), fdatestr = (fdate.getMonth() + 1) + '/' + (fdate.getDate()) + '/' + fdate.getFullYear() + ' ' + printTime(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) : '';
                    h = '<div class=filelist file=999><input file=999 style=float:left name=fc class=fcb type=checkbox onchange=p5setActions() value=\'' + name + '\'>&nbsp;<span style=float:right;padding-right:4px>' + right + '</span><span><div class=fileIcon' + f.t + '></div><a style=cursor:pointer onclick=p5folderset("' + encodeURIComponent(f.nx) + '")>' + shortname + '</a></span></div>';
                } else {
                    var link = shortname;
                    var publiclink = '';
                    if (publicfolder) { publiclink = ' (<a style=cursor:pointer onclick=\'p5showPublicLink("' + publicPath + '/' + f.nx + '")\'>' + "링크" + '</a>)'; }
                    if (f.s > 0) { link = '<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"downloadfile.ashx?link=' + encodeURIComponent(filetreelinkpath + '/' + f.nx) + '\">' + shortname + '</a>' + publiclink; }
                    h = '<div class=filelist file=3><input file=3 style=float:left name=fc class=fcb type=checkbox onchange=p5setActions() value=\'' + f.nx + '\'>&nbsp;<span style=float:right;padding-right:4px>' + EscapeHtml(fsize) + '</span><span><div class=fileIcon' + f.t + '></div>' + link + '</span></div>';
                }

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

            //if (f.parent == null) {  }
            QH('p5rightOfButtons', p5getQuotabar(filetreex));

            QH('p5files', html1 + html2);
            QH('p5currentpath', displayPath);
            QE('p5FolderUp', filetreelocation.length != 0);
            QV('p5PublicShare', publicfolder);

            // Re-check all boxes if needed
            if (oldlinkpath == filetreelinkpath) {
                checkboxes = document.getElementsByName('fc');
                for (var i = 0; i < checkboxes.length; i++) {
                    checkboxes[i].checked = (checkedBoxes.indexOf(checkboxes[i].value) >= 0);
                }
            }

            p5setActions();
        }

        function getNiceSize(bytes) {
            if (bytes <= 0) return "저장 용량 초과";
            if (bytes < 2048) return format("{0}b 남음", bytes);
            if (bytes < 2097152) return format("{0} k 남음", Math.round(bytes / 1024));
            if (bytes < 2147483648) return format("{0}분 남음", Math.round(bytes / 1024 / 1024));
            return format("{0}g 남음", Math.round(bytes / 1024 / 1024 / 1024));
        }

        function p5getQuotabar(f) {
            while (f.t > 1 && f.t != 4) { f = f.parent; }
            if ((f.t != 1 && f.t != 4) || (f.maxbytes == null)) return '';
            return getNiceSize(f.maxbytes - f.s) + ' <progress style=height:10px;width:100px value=' + f.s + ' max=' + f.maxbytes + ' />';
        }

        function p5showPublicLink(u) { setDialogMode(2, "공개 링크", 1, null, '<input type=text style=width:100% value="' + u + '" readonly />'); }

        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('p5NewFolderButton', filetreelocation.length > 0);
            QE('p5UploadButton', filetreelocation.length > 0);
            QE('p5RenameFileButton', (cc == 1) && (filetreelocation.length > 0));
            QE('p5SelectAllButton', tc > 0);
            Q('p5SelectAllButton').value = (cc > 0 ? "없음" : "모두");
            QE('p5CutButton', (sfc > 0) && (cc == sfc));
            QE('p5CopyButton', (sfc > 0) && (cc == sfc));
            QE('p5PasteButton', (p5clipboard != null) && (p5clipboard.length > 0) && (filetreelocation.length > 0));
        }

        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 (size == 1) return "1바이트"; return format("{0} B", 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() { setDialogMode(2, "새 폴더", 3, p5createfolderEx, '<input type=text id=p5renameinput maxlength=64 onkeyup=p5fileNameCheck(event) style=width:100% />'); 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 id=p5recdeleteinput>' + "재귀 삭제" + '</label><br>' : '<input type=checkbox id=p5recdeleteinput style=\'display:none\'>'; setDialogMode(2, "삭제", 3, p5deletefileEx, (cc > 1) ? (format("선택한 항목 {0}을 삭제하시겠습니까?", cc) + rec) : ("선택한 항목을 삭제 하시겠습니까?" + rec)); }
        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; } } setDialogMode(2, "이름 바꾸기", 3, p5renamefileEx, '<input type=text id=p5renameinput maxlength=64 onkeyup=p5fileNameCheck(event) style=width:100% value="' + renamefile + '" />', { action: 'fileoperation', fileop: 'rename', path: filetreelocation, oldname: renamefile }); focusTextBox('p5renameinput'); p5fileNameCheck(); }
        function p5renamefileEx(b, t) { t.newname = Q('p5renameinput').value; meshserver.send(t); }
        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() { setDialogMode(2, "파일 업로드", 3, p5uploadFileEx, '<form method=post enctype=multipart/form-data action=uploadfile.ashx target=fileUploadFrame><input type=text name=link style=display:none id=p5uploadpath value="' + encodeURIComponent(filetreelinkpath) + '" /><input type=file name=files id=p5uploadinput style=width:100% multiple=multiple onchange="updateUploadDialogOk(\'p5uploadinput\')" /><input type=hidden name=authCookie value=' + authCookie + ' /><input type=submit id=p5loginSubmit style=display:none /></form>'); updateUploadDialogOk('p5uploadinput'); }
        function p5uploadFileEx() { Q('p5loginSubmit').click(); }
        function updateUploadDialogOk(x) { QE('idx_dlgOkButton', Q(x).value != ''); }

        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("{2} 항목 {1}의 {0}을 이 위치로 확정하시겠습니까?", (p5clipboardCut == 0 ? 'copy' : 'move'), p5clipboard.length, ((p5clipboard.length > 1) ? 's' : '')) } setDialogMode(2, "붙여넣기", 3, p5pasteFileEx, x); }
        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("{2}에 대해 {1} 항목 {0}을 보유", p5clipboard.length, ((p5clipboard.length > 1) ? 's' : ''), (p5clipboardCut == 0 ? "복사" : "이동")) + ', <a href=# onclick="return p5clearClip()" style=cursor:pointer>' + "지우기" + '</a>.' } QH('p5bottomstatus', x); p5setActions(); }
        function p5clearClip() { p5clipboard = null; p5clipboardFolder = null; p5clipboardCut = 0; p5updateClipview(); return false; }

        function p5fileDragDrop(e) {
            haltEvent(e);
            QV('bigfail', false);
            QV('bigok', false);
            //QV('p5fileCatchAllInput', false);
            if (e.dataTransfer == null || e.dataTransfer.files.length == 0 || filetreelocation.length == 0) return;
            var names = [], sizes = [], types = [], datas = [], readercount = e.dataTransfer.files.length;
            for (var i = 0; i < e.dataTransfer.files.length; i++) {
                var reader = new FileReader(), file = e.dataTransfer.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('*');
                        Q('p5fileDragLink').value = encodeURIComponent(filetreelinkpath);
                        Q('p5loginSubmit2').click();
                    }
                }
                reader.readAsDataURL(file);
            }
        }

        var p5dragtimer = null;
        function p5fileDragOver(e) {
            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) {
            haltEvent(e);
            if (e.target.id != 'p5filetable') {
                QV('bigfail', false);
                QV('bigok', false);
                //QV('p5fileCatchAllInput', false);
            } else {
                p5dragtimer = setTimeout('QV(\'bigfail\',false);QV(\'bigok\',false);p5dragtimer=null;', 200);
            }
        }

        //
        // MY DEVICES
        //

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

        function onOnlineCheckBox(e) {
            putstore('onlineOnly', Q('OnlineCheckBox').checked ? 1 : 0);
            onSearchInputChanged();
        }

        function onDeviceSearchChanged(e) {
            onSearchInputChanged();
        }

        function clearSearchInput() {
            Q('SearchInput').value = '';
            onSearchInputChanged();
        }

        function onSearchInputChanged() {
            var x = Q('SearchInput').value.toLowerCase().trim(); putstore('_search', Q('SearchInput').value);
            var userSearch = null, ipSearch = null, groupSearch = null, tagSearch = null;
            if (x.startsWith("사용자:".toLowerCase())) { userSearch = x.substring("사용자:".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("그룹:".toLowerCase())) { groupSearch = x.substring("그룹:".length); }
            else if (x.startsWith("g:".toLowerCase())) { groupSearch = x.substring("g:".length); }
            else if (x.startsWith("태그:".toLowerCase())) { tagSearch = Q('SearchInput').value.trim().substring("태그:".length); }
            else if (x.startsWith("t:".toLowerCase())) { tagSearch = Q('SearchInput').value.trim().substring("t:".length); }

            if (x == '') {
                // No search
                for (var d in nodes) { nodes[d].v = true; }
            } else if (ipSearch != null) {
                // IP address search
                for (var d in nodes) { nodes[d].v = ((nodes[d].ip != null) && (nodes[d].ip.indexOf(ipSearch) >= 0)); }
            } else if (groupSearch != null) {
                // Group filter
                for (var d in nodes) { nodes[d].v = (meshes[nodes[d].meshid].name.toLowerCase().indexOf(groupSearch) >= 0); }
            } else if (tagSearch != null) {
                // Tag filter
                for (var d in nodes) {
                    nodes[d].v = ((nodes[d].tags == null) && (tagSearch == '')) || ((nodes[d].tags != null) && (nodes[d].tags.indexOf(tagSearch) >= 0));
                }
            } else if (userSearch != null) {
                // User search
                for (var d in nodes) {
                    nodes[d].v = false;
                    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].v = true; } } }
                }
            } else if (x == '*') {
                // Star filter
                for (var d in nodes) { nodes[d].v = (stars[nodes[d]._id] == 1); }
            } else {
                // Device name search
                try {
                    var rs = x.split(/\s+/).join('|'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
                    for (var d in nodes) {
                        nodes[d].v = (rx.test(nodes[d].name.toLowerCase())) || (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase()));
                        if ((nodes[d].v == false) && nodes[d].tags) {
                            for (var s in nodes[d].tags) {
                                if (rx.test(nodes[d].tags[s].toLowerCase())) {
                                    nodes[d].v = true;
                                    break;
                                } else {
                                    nodes[d].v = false;
                                }
                            }
                        }
                    }
                } catch (ex) { for (var d in nodes) { nodes[d].v = true; } }
            }

            // Check power state
            var onlineOnly = Q('OnlineCheckBox').checked;
            if (onlineOnly) { for (var d in nodes) { if ((nodes[d].conn == null) || (nodes[d].conn == 0)) { nodes[d].v = false; } } }
            updateDevices();
        }

        function ondeskkeypress(e) {
            toggleSoftKeys(0);
            Q('DeskSoftInput').value = '';
            setSessionActivity();
            if (desktop && !xxdialogMode && xxcurrentView == 10) {
                // Check what keys we are allows to send
                if (currentNode != null) {
                    var meshrights = GetMeshRights(currentNode.meshid);
                    var inputAllowed = ((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);
            }
        }

        function ondeskkeydown(e) {
            toggleSoftKeys(0);
            Q('DeskSoftInput').value = '';
            setSessionActivity();
            if (desktop && !xxdialogMode && xxcurrentView == 10) {
                // Check what keys we are allows to send
                if (currentNode != null) {
                    var meshrights = GetMeshRights(currentNode.meshid);
                    var inputAllowed = ((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);
            }
        }

        function ondeskkeyup(e) {
            toggleSoftKeys(0);
            Q('DeskSoftInput').value = '';
            setSessionActivity();
            if (desktop && !xxdialogMode && xxcurrentView == 10) {
                // Check what keys we are allows to send
                if (currentNode != null) {
                    var meshrights = GetMeshRights(currentNode.meshid);
                    var inputAllowed = ((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);
            }
        }

        // Since the update device call can be quite frequent, we can moderate it and only call it at most 5 times a second.
        var updateDevicesTimer = null;
        function updateDevices() { if (updateDevicesTimer != null) return; updateDevicesTimer = setTimeout(updateDevicesEx, 200); }

        var sort = 0;
        var deviceHeaderId = 0;
        var deviceHeaderCount;
        var deviceHeaders = {};
        var showRealNames = false;
        var deviceHeaderTotal = 0;
        var deviceHeaders = {};
        var deviceHeadersTitles = {};
        function updateDevicesEx() {
            if (updateDevicesTimer != null) { clearTimeout(updateDevicesTimer); updateDevicesTimer = null; }
            var r = '', c = 0, current = null, count = 0, displayedMeshes = {}, groups = {}, groupCount = {};

            // 3 wide, list view or desktop view
            deviceHeaderId = 0;
            deviceHeaderCount = {};
            deviceHeaderTotal = 0;
            deviceHeaders = {};
            deviceHeadersTitles = {};
            var current;

            // 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); } }

            // Go thru the list of nodes and display them
            for (var i in nodes) {
                if (nodes[i].v == false) continue;
                //var meshrights = GetNodeRights(nodes[i]);

                if (sort == 0) {
                    // Mesh header
                    nodes.sort(meshSort);
                    //if (nodes[i].meshid != current) {
                    if (((meshes[nodes[i].meshid]?nodes[i].meshid:'*') != current)) {
                        deviceHeaderSet();
                        var extra = '';
                        if ((meshes[nodes[i].meshid] != null) && (meshes[nodes[i].meshid].mtype == 1)) { extra = '<span style=color:lightgray>' + ", Intel&reg; AMT 만" + '</span>'; }
                        if (current != null) { if (c == 2) { r += '<td><div style=width:301px></div></td>'; } if (r != '') { r += '</tr></table>'; } }
                        r += '<div class=DevSt style=padding-top:4px><span style=float:right>';
                        //r += getMeshActions(meshes[nodes[i].meshid], meshrights);

                        if (meshes[nodes[i].meshid]) {
                            r += '</span><span id=MxMESH style=cursor:pointer onclick=goForward("' + nodes[i].meshid + '")>' + EscapeHtml(meshes[nodes[i].meshid].name) + '</span>' + extra + '<span id=DevxHeader' + deviceHeaderId + ' style=color:lightgray></span></div>';
                            current = nodes[i].meshid;
                        } else {
                            r += '</span><span id=MxMESH><i>' + "개별 장치" + '</i></span><span id=DevxHeader' + deviceHeaderId + ' style=color:lightgray></span></div>';
                            current = '*';
                        }
                        
                        displayedMeshes[current] = 1;
                        c = 0;
                    }
                } else if (sort == 1) {
                    // Power header
                    if (nodes[i].pwr !== current) {
                        deviceHeaderSet();
                        if (current !== null) { if (c == 2) { r += '<td><div style=width:301px></div></td>'; } if (r != '') { r += '</tr></table>'; } }
                        r += '<div class=DevSt style=width:100%;padding-top:4px><span>' + PowerStateStr2(nodes[i].pwr) + '</span><span id=DevxHeader' + deviceHeaderId + ' style=color:lightgray></span></div>';
                        current = nodes[i].pwr;
                        c = 0;
                    }
                } else if (sort == 2) {
                    // Device header
                    if (current == null) { current = '1'; }
                }

                count++;
                r += '<div name=xxdevice class=devList1 id=\'' + nodes[i]._id + '\'></div>'; // This is a standin for the device, it gets rendered only if visible.

                // If we are displaying devices by group, put the device in the right group.
                /*
                if ((sort == 3) && (r != '')) {
                    if (nodes[i].tags) {
                        for (var j in nodes[i].tags) {
                            var tag = nodes[i].tags[j];
                            if (groups[tag] == null) { groups[tag] = r; groupCount[tag] = 1; } else { groups[tag] += r; groupCount[tag] += 1; }
                            if (view == 3) break;
                        }
                    }
                    r = '';
                }
                */

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

            // If there is nothing to display, explain the problem
            var viewNothing = false;
            if ((r == '') && (nodes.length > 0) && (Q('SearchInput').value != '')) {
                viewNothing = true;
                r = '<div style="margin:30px">' + "이 검색과 일치하는 기기가 없습니다." + '</div>';
            }

            // 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 == '')) {
                for (var i in meshes) {
                    var mesh = meshes[i];
                    if ((displayedMeshes[mesh._id] == null) && (IsMeshViewable(mesh))) {
                        if ((current != '') && (r != '')) { r += '</tr></table>'; }
                        r += '<div><div colspan=3 class=DevSt><span style=float:right>';
                        //r += getMeshActions(mesh, meshrights);
                        r += '</span><span id=MxMESH style=cursor:pointer onclick=goForward("' + mesh._id + '")>' + EscapeHtml(mesh.name) + '</span></div>';
                        if (mesh.mtype == 1) { r += '<div style=padding:10px><i>' + "이 그룹에 Intel&reg; AMT 장치가 없습니다"; }
                        if (mesh.mtype == 2) { r += '<div style=padding:10px><i>' + "이 그룹에 기기가 없습니다."; }
                        r += '.</i></div></div>';
                        current = mesh._id;
                        count++;
                    }
                }
            }

            if (count == 0) {
                QH('xdevices', '<div style="margin-top:50px;text-align:center"><span style="font-size:30px">' + "기기가 없습니다" + '</span><br /><br />' + "이 웹 사이트의 데스크탑 버전을 사용하여 장치를 추가하십시오." + '</div>');
            } else {
                QH('xdevices', r);
            }
            deviceHeaderSet();
            for (var i in deviceHeaders) { QH(i, deviceHeaders[i]); }
            for (var i in deviceHeadersTitles) { Q(i).title = deviceHeadersTitles[i]; }
            onDevicesScrollEx();
        }

        var onDevicesTouchActive = false;
        var onDevicesScrollnagleTimer = null;
        function onDevicesScroll() {
            if (onDevicesScrollnagleTimer != null) { clearTimeout(onDevicesScrollnagleTimer); onDevicesScrollnagleTimer = null; }
            if (onDevicesTouchActive) return;
            onDevicesScrollnagleTimer = setTimeout(onDevicesScrollEx, 50);
        }

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

        function onDevicesScrollEx() {
            onDevicesScrollnagleTimer = null;
            var visibleTop = Q('xdevices').scrollTop - 250, visibleBottom = Q('xdevices').scrollTop + Q('xdevices').clientHeight + 250, devdivs = document.getElementsByName('xxdevice');
            for (var i = 0; i < devdivs.length; i++) {
                if ((devdivs[i].offsetTop >= visibleTop) && (devdivs[i].offsetTop < visibleBottom)) {
                    // Show
                    var node = getNodeFromId(devdivs[i].id)
                    if (node == null) break;

                    var title = EscapeHtml(node.name);
                    if (title.length == 0) { title = '<i>' + "없음" + '</i>'; }
                    if ((node.rname != null) && (node.rname.length > 0)) { title += ' / ' + EscapeHtml(node.rname); }
                    var name = EscapeHtml(node.name);
                    if (showRealNames == true && node.rname != null) name = EscapeHtml(node.rname);
                    if (name.length == 0) { name = '<i>' + "없음" + '</i>'; }

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

                    // This device is "starred"
                    if (stars[node._id] == 1) {
                        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) {
                            devNotifySub += '<div 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)) {
                            devNotifySub += '<img class=deviceNotifyDotSub src=images/icon-relay-notify.png width=16 height=16>';
                        }

                        // Battery state
                        if (node.sessions.battery != null) {
                            var bat = node.sessions.battery;
                            var statestr = '';
                            if (bat.state == 'ac') { statestr = "Device is plugged-in"; }
                            else if (bat.state == 'dc') { statestr = "Device is battery powered"; }

                            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 != '') { devNotify += '<div class=deviceNotifyDot>' + devNotifySub + '</div>'; }

                    // Node
                    var icon = node.icon, nodestate = NodeStateStr(node);
                    if ((!node.conn) || (node.conn == 0)) { icon += ' gray'; }
                    devdivs[i].innerHTML = '<div onclick=goForward(\'' + node._id + '\')>' + devNotify + '<div class="i' + icon + ' devList2"></div><div class=devList3><div class=devList4><b>' + name + '</b></div><div class=devList5>' + nodestate + '</div></div></div>';
                } else {
                    devdivs[i].innerHTML = ''; // Hide
                }
            }
        }

        // Show currently active sessions on this device
        function showDeviceSessions() {
            if (xxdialogMode || (currentNode == null)) return;
            var node = currentNode, x = '';
            for (var i in node.sessions) {
                if ((i == 'kvm') && (node.sessions.multidesk == null)) {
                    x += '<u>' + "Remote Desktop" + '</u>';
                    for (var j in node.sessions.kvm) { x += addHtmlValue4(getUserName(j), (node.sessions.kvm[j] == 1) ? "1세션" : nobreak(format("{0} 세션", node.sessions.kvm[j]))); }
                } else if (i == 'multidesk') {
                    x += '<u>' + "Remote Desktop" + '</u>';
                    for (var j in node.sessions.multidesk) { x += addHtmlValue4(getUserName(j), ((node.sessions.multidesk[j] == 1) ? "1세션" : nobreak(format("{0} 세션", node.sessions.multidesk[j])))); }
                } else if (i == 'terminal') {
                    x += '<u>' + "터미널" + '</u>';
                    for (var j in node.sessions.terminal) { x += addHtmlValue4(getUserName(j), ((node.sessions.terminal[j] == 1) ? "1세션" : nobreak(format("{0} 세션", node.sessions.terminal[j])))); }
                } else if (i == 'files') {
                    x += '<u>' + "파일" + '</u>';
                    for (var j in node.sessions.files) { x += addHtmlValue4(getUserName(j), ((node.sessions.files[j] == 1) ? "1세션" : nobreak(format("{0} 세션", node.sessions.files[j])))); }
                } else if (i == 'tcp') {
                    x += '<u>' + "TCP Routing" + '</u>';
                    for (var j in node.sessions.tcp) { x += addHtmlValue4(getUserName(j), ((node.sessions.tcp[j] == 1) ? "1세션" : nobreak(format("{0} 세션", node.sessions.tcp[j])))); }
                } else if (i == 'udp') {
                    x += '<u>' + "UDP Routing" + '</u>';
                    for (var j in node.sessions.udp) { x += addHtmlValue4(getUserName(j), ((node.sessions.udp[j] == 1) ? "1세션" : nobreak(format("{0} 세션", node.sessions.udp[j])))); }
                }
            }
            if (x != '') setDialogMode(2, "Sessions" + ' - ' + EscapeHtml(node.name), 1, null, x, 'SESSIONS-' + node._id);
        }

        // 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;width:100%;overflow-y:auto;overflow-x:hidden>', 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].icon == 'number') { icon = node.sessions.msg[i].icon; }
                if ((icon < 1) || (icon > 9)) { icon = 5; }
                x += '<table style=width:100%><td style=width:24px><div class=NotifyIconSmall' + icon + '></div><td><div style="border-radius:5px;background-color:#BBB;width:calc(100% - 18px);padding:8px">' + EscapeHtml(msg) + '</div></table>';
                count++;
            }
            x += '</div>';
            if (count > 0) setDialogMode(2, "Agent Messages" + ' - ' + EscapeHtml(node.name), 1, null, x, 'MESSAGES-' + node._id);
            return false;
        }

        var powerStatetable = ['', "전원이 켜짐", "절전", "절전", "절전", "최대 절전 모드", "전원 끄기", "현재"];
        var powerStateStrings = ['', "전원이 켜짐", "절전 중", "절전 중", "최대 절전 모드", "최대 절전 모드", "소프트-오프", "현재"];
        var powerStateStrings2 = ['', "장치의 전원이 공급되었습니다.", "장치가 절전 상태입니다 (S1)", "장치가 절전 상태입니다 (S2)", "장치가 최대 절전 상태입니다 (S3).", "장치가 최대 절전 모드입니다 (S4).", "장치가 소프트 오프 상태입니다 (S5)", "장치가 있지만 전원 상태를 확인할 수 없습니다."];
        var powerColorTable = ['#00000000', 'black', 'blue', 'blue', 'lightblue', 'blueviolet', 'darkgreen', 'lightseagreen', 'lightseagreen'];
        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) { states.push('<span>' + "에이전트" + '</span>'); }
                if ((node.conn & 2) != 0) { states.push('<span>' + "CIRA" + '</span>'); }
                else if ((node.conn & 4) != 0) { states.push('<span>' + "Intel&reg; AMT" + '</span>'); }
                if ((node.conn & 8) != 0) { states.push('<span>' + "전달(Relay)" + '</span>'); }
                if ((node.conn & 16) != 0) { states.push('<span>' + "MQTT" + '</span>'); }
            }
            if ((node.pwr != null) && (node.pwr != 0)) { states.push(powerStateStrings[node.pwr]); }
            return states.join(', ');
        }

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

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

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

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

        function meshSort(a, b) { if (a.meshnamel > b.meshnamel) return 1; if (a.meshnamel < b.meshnamel) return -1; if (a.meshid == b.meshid) { 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 powerSort(a, b) { var ap = a.pwr ? a.pwr : 0; var bp = b.pwr ? b.pwr : 0; 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; } } if (ap > bp) return 1; if (ap < bp) return -1; 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; }

        //
        // MY DEVICE
        //

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

        var currentDevicePanel = 0;
        var currentNode;
        var powerTimelineNode = null;
        var powerTimelineReq = null;
        var powerTimelineUpdate = null;
        var powerTimeline = null;
        function getCurrentNode() { return currentNode; };
        function gotoDevice(nodeid, panel, refresh) {

            // Remind the user to verify the email address
            if ((userinfo.emailVerified !== true) && (serverinfo.emailcheck == true) && (userinfo.siteadmin != 0xFFFFFFFF)) { setDialogMode(2, "계정 보안", 1, null, "이메일 주소가 확인 될 때까지 장치에 액세스 할 수 없습니다. 이것은 비밀번호 복구에 필요합니다. 이메일 주소를 변경하고 확인하려면 \"내 계정\"으로 이동하십시오."); return; }

            // Remind the user to add two factor authentication
            if ((features & 0x00040000) && !((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0) || (userinfo.otpkeys > 0) || ((features & 0x00800000) && (userinfo.otpekey == 1)))) { setDialogMode(2, "계정 보안", 1, null, "2단계 인증이 활성화 될 때까지 장치에 액세스 할 수 없습니다. 이것은 추가 보안을 위해 필요합니다. \"내 계정\" 탭으로 이동하여 \"계정 보안\" 부분을 보십시오."); return; }

            var node = getNodeFromId(nodeid);
            if (node == null) { goBack(); return; }
            var mesh = meshes[node.meshid];
            var meshrights = GetNodeRights(node);
            if (!currentNode || currentNode._id != node._id || refresh == true) {
                currentNode = node;

                // Setup session notification
                QV('p10deviceNotify', (currentNode.sessions != null) && ((node.sessions.kvm != null) || (node.sessions.terminal != null) || (node.sessions.files != null) || (node.sessions.tcp != null) || (node.sessions.udp != null)));
                QV('p10deviceStar', stars[currentNode._id] == 1);
                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 = "Device is plugged-in"; }
                    if (bat.state == 'dc') { statestr = "Device is battery powered"; }

                    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);
                }

                // Add node name
                var nname = EscapeHtml(node.name);
                if (nname.length == 0) { nname = '<i>' + "없음" + '</i>'; }
                if ((meshrights & 4) != 0) { nname = '<span onclick=showEditNodeValueDialog(0) style=cursor:pointer>' + nname + '</span>'; }
                QH('p10deviceName', nname);

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

                // Attribute: Mesh
                if (mesh) { x += addDeviceAttribute('<span>' + "그룹" + '</span>', '<a onclick=goForward("' + node.meshid + '") style=cursor:pointer>' + EscapeHtml(meshes[node.meshid].name) + '</a>'); }

                // Attribute: Name
                if (node.rname != null) { x += addDeviceAttribute('<span>' + "이름" + '</span>', '<span>' + EscapeHtml(node.rname) + '</span>'); }

                // Attribute: Host
                if ((features & 1) == 0) { // If not WAN-only, local hostname is in use
                    if ((meshrights & 4) != 0) {
                        if (node.host) {
                            x += addDeviceAttribute("호스트 이름", '<span onclick=showEditNodeValueDialog(1) style=cursor:pointer>' + EscapeHtml(node.host) + '</span>');
                        } else {
                            x += addDeviceAttribute("호스트 이름", '<span onclick=showEditNodeValueDialog(1) style=cursor:pointer><i>' + "없음" + '</i></span>');
                        }
                    } else {
                        x += addDeviceAttribute("호스트 이름", EscapeHtml(node.host));
                    }
                }

                // Attribute: Description
                var description = node.desc ? EscapeHtml(node.desc) : '<i>' + "없음" + '</i>';
                if ((meshrights & 4) != 0) {
                    x += addDeviceAttribute("설명", '<span onclick=showEditNodeValueDialog(2) style=cursor:pointer>' + description + '</span>');
                } else {
                    x += addDeviceAttribute("설명", description);
                }

                // Attribute: Mesh Agent
                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; }
                    x += addDeviceAttribute("에이전트", str);
                }

                // Attribute: Intel AMT
                if (node.intelamt != null) {
                    var str = '';
                    var provisioningStates = { 0: nobreak("활성화되지 않음 (Pre)"), 1: nobreak("활성화되지 않음 (In)"), 2: nobreak("활성화 됨") };
                    if (node.intelamt.ver != null && node.intelamt.state == null) { str += '<i>' + nobreak("알 수 없는 상태") + '</i>, v' + EscapeHtml(node.intelamt.ver); } else

                        if ((node.intelamt.ver == null) && (node.intelamt.state == 2)) { str += '<i>' + "활성화 됨" + '</i>'; }
                        else if ((node.intelamt.ver == null) || (node.intelamt.state == null)) { str += '<i>' + "알 수 없는 버전 및 상태" + '</i>'; }
                        else {
                            str += provisioningStates[node.intelamt.state];
                            if (node.intelamt.flags) { if (node.intelamt.flags & 2) { str = ' <span>' + "CCM" + '</span>'; } else if (node.intelamt.flags & 4) { str = ' <span>' + "ACM" + '</span>'; } }
                            str += (', v' + EscapeHtml(node.intelamt.ver));
                        }

                    if (node.intelamt.tls == 1) { str += ', <span>' + "TLS" + '</span>'; }
                    if (node.intelamt.state == 2) {
                        if (node.intelamt.user == null || node.intelamt.user == '') {
                            if ((meshrights & 4) != 0) {
                                str += ', <i style=color:#FF0000;cursor:pointer onclick=editDeviceAmtSettings("' + node._id + '")>' + nobreak("자격 증명이 없습니다.") + '</i>';
                            } else {
                                str += ', <i style=color:#FF0000>' + "자격 증명이 없습니다." + '</i>';
                            }
                        }
                        str += ' ';
                        if ((meshrights & 4) != 0) {
                            str += '<img src=images/link4.png height=10 width=10 style=cursor:pointer onclick=editDeviceAmtSettings("' + node._id + '")>';
                        }
                    }

                    var meName = "Intel&reg; ME";
                    if (typeof node.intelamt.sku == 'number') {
                        if ((node.intelamt.sku & 8) != 0) { meName = "Intel&reg; AMT"; }
                        else if ((node.intelamt.sku & 16) != 0) { meName = "Intel&reg; SM"; }
                    }
                    x += addDeviceAttribute(meName, str);
                }

                // Attribute: Mesh Agent Tag
                if ((node.agent != null) && (node.agent.tag != null) && (node.agent.tag != 'mailto:')) {
                    var tag = EscapeHtml(node.agent.tag);
                    if (tag.startsWith('mailto:')) { tag = '<a href="' + tag + '">' + tag.substring(7) + '</a>'; }
                    x += addDeviceAttribute("에이전트 태그", tag);
                }

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

                // 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>' + "에이전트" + '</span>');
                    if ((node.conn & 2) != 0) cstate.push('<span>' + "Intel&reg; AMT CIRA" + '</span>');
                    else if ((node.conn & 4) != 0) cstate.push('<span>' + "Intel&reg; AMT" + '</span>');
                    if ((node.conn & 8) != 0) cstate.push('<span>' + "에이전트 전달(Relay)" + '</span>');
                    if ((node.conn & 16) != 0) cstate.push('<span>' + "MQTT" + '</span>');
                    x += addDeviceAttribute("연결성", cstate.join(', '));
                }

                // Node tags
                var groupingTags = '<i>' + "없음" + '</i>';
                if (node.tags != null) { groupingTags = ''; for (var i in node.tags) { groupingTags += '<span style="background-color:lightgray;padding:3px;margin-right:4px;border-radius:5px">' + EscapeHtml(node.tags[i]) + '</span>'; } }
                if ((meshrights & 4) != 0) {
                    x += addDeviceAttribute("태그", '<span onclick=showEditNodeValueDialog(3) style=cursor:pointer>' + groupingTags + '</span>');
                } else {
                    x += addDeviceAttribute("태그", groupingTags);
                }

                x += '</table><br />';
                // Show action button, only show if we have permissions 4, 8, 64
                if ((meshrights & 76) != 0) { x += '<input type=button value=\"' + "동작" + '\" onclick=deviceActionFunction() />'; }
                //x += '<input type=button value=Notes onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponent(node._id) + '") />';
                //if ((connectivity & 1) && (meshrights & 8) && (node.agent.id < 5)) { x += '<input type=button value=Toast onclick=deviceToastFunction() />';  }
                QH('p10html', x);

                // Show node last 7 days timeline
                //drawDeviceTimeline();
                setupFiles();

                // Show bottom buttons
                x = '<div style=float:right;font-size:x-small;margin-right:10px>';
                if ((meshrights & 0x8000) != 0) { x += '<a style=cursor:pointer onclick=p10showDeleteNodeDialog("' + node._id + '")>' + "장치 삭제" + '</a>'; }
                x += '</div><div style=font-size:x-small>';
                //if (mesh.mtype == 2) x += '<a style=cursor:pointer onclick=p10showNodeNetInfoDialog("' + node._id + '")>Interfaces</a>&nbsp;';
                //if (xxmap != null) x += '<a style=cursor:pointer onclick=p10showNodeLocationDialog("' + node._id + '")>Location</a>&nbsp;';
                x += '</div><br>'

                QH('p10html3', x);

                // Set the node power state
                var powerstate = PowerStateStr(node.state);
                //if (node.state == 0) { powerstate = 'Unknown State'; }
                if ((connectivity & 1) != 0) { if (powerstate.length > 0) { powerstate += ', '; } powerstate += '<span style=font-size:10px>' + "메시 에이전트" + '</span>'; }
                if ((connectivity & 2) != 0) { if (powerstate.length > 0) { powerstate += ', '; } powerstate += '<span style=font-size:10px>' + "Intel&reg; AMT 연결" + '</span>'; }
                else if ((connectivity & 4) != 0) { if (powerstate.length > 0) { powerstate += ', '; } powerstate += '<span style=font-size:10px>' + "Intel&reg; AMT 감지" + '</span>'; }
                if ((connectivity & 16) != 0) { if (powerstate.length > 0) { powerstate += '<br/>'; } powerstate += '<span style=font-size:12px>' + "MQTT 채널 연결" + '</span>'; }
                QH('MainComputerState', powerstate);

                // Set the node icon
                QH('MainComputerImage', '<div class="i' + node.icon + '"></div>');

                // 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: 'getsysinfo', nodeid: currentNode._id });
                    meshserver.send({ action: 'getnetworkinfo', nodeid: currentNode._id });
                    QH('p10detailshtml', '');
                }
            }
            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);

            // Update the footer menu
            if (xxcurrentView == 10) { setupDeviceMenu(); }
        }

        function deviceToastFunction() {
            if (xxdialogMode) return;
            setDialogMode(2, "장치 토스트", 3, deviceToastFunctionEx, '<textarea id=d2devToast style=width:100%;height:80px;resize:none;overflow-y:scroll></textarea>');
        }

        function deviceToastFunctionEx() {
            meshserver.send({ action: 'toast', nodeids: [currentNode._id], title: 'MeshCentral', msg: Q('d2devToast').value });
        }

        // && ((meshrights == 0xFFFFFFFF) || ((meshrights & 65536) == 0))
        function setupDeviceMenu(op, obj) {
            var meshrights = GetNodeRights(currentNode);
            if (op != null) { currentDevicePanel = op; }
            QV('p10general', currentDevicePanel == 0);
            QV('p10desktop', currentDevicePanel == 1); // Show if we have remote control rights or desktop view only rights
            QV('p10files', currentDevicePanel == 2);
            QV('p10details', currentDevicePanel == 3);
            var menus = [];
            if (currentDevicePanel != 0) { menus.push({ n: "일반", f: 'setupDeviceMenu(0)' }); }

            if ((currentDevicePanel != 1) &&
                (currentNode != null) &&
                ((meshrights & 8) || (meshrights & 256)) && ((meshrights == 0xFFFFFFFF) || ((meshrights & 65536) == 0)) &&
                (((currentNode.agent == null) && ((typeof currentNode.intelamt.sku !== 'number') || ((currentNode.intelamt.sku & 8) != 0))) || (currentNode.agent && (currentNode.agent.caps & 1)))
            ) { menus.push({ n: "데스크탑", f: 'setupDeviceMenu(1)' }); }

            if ((currentDevicePanel != 2) && (currentNode != null) && (meshrights & 8) && ((meshrights == 0xFFFFFFFF) || ((meshrights & 1024) == 0)) && ((currentNode.mtype == 2) && (currentNode.agent.caps & 4))) { menus.push({ n: "파일", f: 'setupDeviceMenu(2)' }); }
            if ((currentDevicePanel != 3) && (currentNode != null)) { menus.push({ n: "세부사항", f: 'setupDeviceMenu(3)' }); }
            updateFooterMenu(menus);
        }

        function deviceActionFunction() {
            if (xxdialogMode) return;
            var meshrights = GetNodeRights(currentNode);
            var x = "이 장치에서 수행할 작업을 선택하십시오." + '<br /><br />';
            var y = '<select id=d2deviceop style=float:right;width:170px>';
            if ((meshrights & 64) != 0) { y += '<option value=100>' + "깨우기(Wake-up)" + '</option>'; } // Wake-up permission
            if ((meshrights & 8) != 0) { y += '<option value=4>' + "절전" + '</option><option value=3>' + "초기화" + '</option><option value=2>' + "전원 끄기" + '</option>'; } // Remote control permission
            y += '</select>';
            x += addHtmlValue("조작", y);
            setDialogMode(2, "장치 동작", 3, deviceActionFunctionEx, x);
        }

        function deviceActionFunctionEx() {
            var op = Q('d2deviceop').value;
            if (op == 100) {
                // Device wake
                meshserver.send({ action: 'wakedevices', nodeids: [currentNode._id] });
            } else {
                // Power operation
                meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: op });
            }
        }

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

        // Draw device power bars. The bars are 766px wide.
        function drawDeviceTimeline() {
            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('masthead').offsetWidth - (90 + 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) { datavalue += '<div style=display:table-cell;width:' + width + 'px;background-color:' + powerColor(block[2]) + ';height:16px></div>'; }
                    }
                }
                x += '<tr style=' + (((count % 2) == 0) ? 'background-color:#DDD' : '') + '><td><div>&nbsp;' + printDate(date) + '<div></div></div></td><td><div>' + datavalue + '</div></td></tr>';
                ++count;
                date = new Date(date.getTime() - (1000 * 60 * 60 * 24)); // Substract one day
            }
            QH('p10html2', '<table style="color:black;background-color:#EEE;border-color:#AAA;border-width:1px;border-style:solid;border-collapse:collapse;width:calc(100% - 18px);margin:9px" border=0 cellpadding=2 cellspacing=0><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col style=text-align:center;width:90px>' + "일" + '</th><th scope=col style=text-align:center>' + "Power State" + '</th></tr>' + x + '</tbody></table>');
        }

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

        // 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) {
            return '<tr><td style=width:100px;color:gray>' + name + '</td><td style=overflow:hidden>' + value + '</td></tr>';
        }

        function editDeviceAmtSettings(nodeid, func) {
            if (xxdialogMode) return;
            var x = '', node = getNodeFromId(nodeid), buttons = 3, meshrights = GetNodeRights(node);
            if ((meshrights & 4) == 0) return;
            x += addHtmlValue("사용자 이름", '<input id=dp10username style=width:170px maxlength=32 autocomplete=nope placeholder="admin" onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            x += addHtmlValue("암호", '<input id=dp10password type=password style=width:170px autocomplete=nope maxlength=32 onchange=validateDeviceAmtSettings() onkeyup=validateDeviceAmtSettings() />');
            x += addHtmlValue("보안", '<select id=dp10tls style=width:176px><option value=0>' + "TLS 보안이 없습니다." + '</option><option value=1>' + "TLS 보안 필요" + '</option></select>');
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { buttons = 7; }
            setDialogMode(2, "Intel&reg; AMT 자격 증명 편집", buttons, editDeviceAmtSettingsEx, x, { node: node, func: func });
            if ((node.intelamt.user != null) && (node.intelamt.user != '')) { Q('dp10username').value = node.intelamt.user; } else { Q('dp10username').value = 'admin'; }
            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 = '';
                meshserver.send({ action: 'changedevice', nodeid: tag.node._id, intelamt: { user: amtuser, pass: amtpass, tls: parseInt(Q('dp10tls').value) } });
                tag.node.intelamt.user = amtuser;
                tag.node.intelamt.tls = parseInt(Q('dp10tls').value);
                if (tag.func) { setTimeout(tag.func, 300); }
            }
        }

        function p10showDeleteNodeDialog(nodeid) {
            if (xxdialogMode) return;
            setDialogMode(2, "기기 삭제", 3, p10showDeleteNodeDialogEx, format("{0}을 삭제 하시겠습니까?", EscapeHtml(currentNode.name)) + '<br /><br /><label><input id=p10check type=checkbox onchange=p10validateDeleteNodeDialog() />' + "확인" + '</label>', nodeid);
            p10validateDeleteNodeDialog();
        }

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

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

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

            var x = '<table align=center><td style=text-align:center>';
            x += '<div style=display:inline-block class=i1 onclick=p10setIcon(1)></div>';
            x += '<div style=display:inline-block class=i2 onclick=p10setIcon(2)></div>';
            x += '<div style=display:inline-block class=i3 onclick=p10setIcon(3)></div>';
            x += '<div style=display:inline-block class=i4 onclick=p10setIcon(4)></div><br />';
            x += '<div style=display:inline-block class=i5 onclick=p10setIcon(5)></div>';
            x += '<div style=display:inline-block class=i6 onclick=p10setIcon(6)></div>';
            x += '<div style=display:inline-block class=i7 onclick=p10setIcon(7)></div>';
            x += '<div style=display:inline-block class=i8 onclick=p10setIcon(8)></div></table>';
            setDialogMode(2, "아이콘 선택", 0, null, x);
            QV('id_dialogclose', true);
        }

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

        var showEditNodeValueDialog_modes = ["장치 이름", "호스트 이름", "설명", "태그"];
        var showEditNodeValueDialog_modes2 = ['name', 'host', 'desc', 'tags'];
        var showEditNodeValueDialog_modes3 = ['', '', '', "그룹1, 그룹2, 그룹3"];
        function showEditNodeValueDialog(mode) {
            if (xxdialogMode) return;
            var x = addHtmlValue(showEditNodeValueDialog_modes[mode], '<input id=dp10devicevalue style=width:170px maxlength=64 placeholder="' + showEditNodeValueDialog_modes3[mode] + '" onchange=p10editdevicevalueValidate(' + mode + ',event) onkeyup=p10editdevicevalueValidate(' + mode + ',event) />');
            setDialogMode(2, "장치 편집", 3, showEditNodeValueDialogEx, x, mode);
            var v = currentNode[showEditNodeValueDialog_modes2[mode]];
            if (v == null) v = '';
            if (Array.isArray(v)) { v = v.join(', '); }
            Q('dp10devicevalue').value = v;
            p10editdevicevalueValidate();
            Q('dp10devicevalue').focus();
        }

        function showEditNodeValueDialogEx(button, mode) {
            var x = { action: 'changedevice', nodeid: currentNode._id };
            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 desktop;
        var desktopNode;
        var desktopsettings = { encoding: 2, showfocus: false, showmouse: true, showcad: true, quality: 40, scaling: 1024, framerate: 50 };
        function setupDesktop() {
            // Setup the remote desktop
            if ((desktopNode != currentNode) && (desktop != null)) { desktop.Stop(); desktopNode = null; desktop = null; }

            // If the device desktop is already connected in multi-desktop, use that.
            if ((desktopNode != currentNode) || (desktop == null)) {
                // Device is not already connected, just setup a blank canvas
                QH('DeskParent', '<canvas id=Desk width=640 height=200 style="width:100%;-ms-touch-action:none;margin-left:0px" oncontextmenu="return false" onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(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;
            updateDesktopButtons();

            // On some browsers like IE, we can't save screen shots. Hide the scheenshot/capture buttons.
            if (!Q('Desk')['toBlob']) { QV('deskSaveBtn', false); }
        }

        // Show and enable the right buttons
        function updateDesktopButtons() {
            var mesh = meshes[currentNode.meshid];
            var deskState = 0;
            if (desktop != null) { deskState = desktop.State; }
            var meshrights = GetNodeRights(currentNode);

            // Show the right buttons
            QV('disconnectbutton1', (deskState != 0));
            QV('connectbutton1', (deskState == 0) && ((meshrights & 8) || (meshrights & 256)) && (currentNode.agent != null) && (currentNode.agent.caps & 1));
            QV('connectbutton1h',
                (deskState == 0) &&
                (meshrights & 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('d7amtkvm', (currentNode.intelamt != null && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
            QV('d7meshkvm', ((currentNode.agent != null) && (currentNode.agent.caps & 1) && ((deskState == false) || (desktop.contype == 1))));

            // Enable buttons
            var online = ((currentNode.conn & 1) != 0); // If Agent (1) connected, enable remote desktop
            QE('connectbutton1', online);
            var hwonline = ((currentNode.conn & 6) != 0); // If CIRA (2) or AMT (4) connected, enable hardware terminal
            QE('connectbutton1h', hwonline);
            //QE('deskSaveBtn', deskState == 3);
            //QV('DeskCAD', meshrights & 8);
            //QE('DeskCAD', deskState == 3);
            //QV('DeskWD', (currentNode.agent) && (currentNode.agent.id < 5));
            //QE('DeskWD', deskState == 3);
            //QV('deskkeys', (currentNode.agent) && (currentNode.agent.id < 5));
            //QE('deskkeys', deskState == 3);
            //QE('DeskToolsButton', online);
            QV('DeskToastButton', ((meshrights & 16384) != 0) && (currentNode.agent) && (currentNode.agent.id < 5) && (meshrights & 8));
            //QE('DeskToastButton', online);
            QV('deskActionsBtn', meshrights & 8);
            Q('DeskControl').checked = ((meshrights & 8) != 0);
            if (online == false) QV('DeskTools', false);
        }

        function connectDesktop(e, contype) {
            setSessionActivity();
            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); 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.showmouse = desktopsettings.showmouse;
                    desktop.m.onScreenSizeChange = deskAdjust;
                    desktop.Start(desktopNode._id, 16994, '*', '*', 0);
                    desktop.contype = 2;
                } else {
                    // Setup the Mesh Agent remote desktop
                    desktop = CreateAgentRedirect(meshserver, CreateAgentRemoteDesktop('Desk'), serverPublicNamePort, authCookie, authRelayCookie, domainUrl);
                    desktop.debugmode = debugmode;
                    desktop.m.debugmode = debugmode;
                    desktop.attemptWebRTC = attemptWebRTC;
                    desktop.onStateChanged = onDesktopStateChange;
                    desktop.m.CompressionLevel = desktopsettings.quality; // Number from 1 to 100. 50 or less is best.
                    desktop.m.ScalingLevel = desktopsettings.scaling;
                    desktop.m.FrameRateTimer = desktopsettings.framerate;
                    desktop.m.onDisplayinfo = deskDisplayInfo;
                    desktop.m.onScreenSizeChange = deskAdjust;
                    desktop.Start(desktopNode._id);
                    desktop.contype = 1;
                }
            } else {
                // Disconnect and clean up the remote desktop
                desktop.Stop();
                desktopNode = desktop = null;
            }
        }

        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.m.stopInput == true) { str += ', Loopback'; }
            QH('deskstatus', str);
            switch (state) {
                case 0:
                    // Disconnect and clean up the remote desktop
                    desktop.Stop();
                    desktopNode = desktop = null;
                    QV('DeskScreens', false);
                    if (fullscreen == true) { deskToggleFull(); }
                    break;
                case 2:
                    break;
                default:
                    //console.log('Unknown onDesktopStateChange state', state);
                    break;
            }
            updateDesktopButtons();
            deskAdjust();
            setTimeout(deskAdjust, 50);
        }

        function showDesktopSettings() {
            if (xxdialogMode) return;
            applyDesktopSettings();
            updateDesktopButtons();
            setDialogMode(7, "원격 데스크톱 설정", 3, showDesktopSettingsChanged);
        }

        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;
            localStorage.setItem('desktopsettings', JSON.stringify(desktopsettings));
            applyDesktopSettings();
            if (desktop) {
                if (desktop.contype == 1) {
                    if (desktop.State != 0) { desktop.m.SendCompressionLevel(1, desktopsettings.quality, desktopsettings.scaling, desktopsettings.framerate); }
                }
                if (desktop.contype == 2) {
                    if (desktop.State != 0) { desktop.Stop(); setTimeout(function () { connectDesktop(null, 2); }, 50); }
                }
            }
        }

        function applyDesktopSettings() {
            var r = '', ops = (features & 512) ? [90, 70, 50, 40, 30, 20, 10, 5, 1] : [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;
            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; }
        }

        var fullscreen = false;
        /*
        function deskToggleFull() {
            fullscreen = !fullscreen;
            QV('mastheadx', !fullscreen);
            QV('masthead', !fullscreen);
            QV('topbar', !fullscreen);
            QV('p11deviceNameHeader', !fullscreen);
            QV('footer', !fullscreen);
            QV('column_l_bottomgap', !fullscreen);
            QV('idx_deskFullBtn2', fullscreen);
            QV('deskFullBtn', !fullscreen);
            if (fullscreen) {
                QS('container').width = '100%';
                QS('container')['border-right'] = '0';
                QS('container')['border-left'] = '0';
                QS('column_l').padding = '0';
                QS('column_l').width = '100%';
            } else {
                QS('container').width = '960px';
                QS('container')['border-right'] = '1px solid #b7b7b7';
                QS('container')['border-left'] = '1px solid #b7b7b7';
                QS('column_l').padding = '0 15px';
                QS('column_l').width = '930px';
                toggleFullScreen();
            }
            deskAdjust();
        }
        */

        function deskAdjust() {
            var x = (Q('DeskParent').clientHeight - Q('Desk').clientHeight) / 2;
            if (x < 0) {
                var mh = Q('DeskParent').clientHeight, mw = 9999;
                if (desktop) { mw = (desktop.m.width / desktop.m.height) * mh; }
                QS('Desk')['max-height'] = mh + 'px';
                QS('Desk')['max-width'] = mw + 'px';
                x = 0;
            } else {
                QS('Desk')['max-height'] = null;
                QS('Desk')['max-width'] = null;
            }
            QS('Desk')['margin-top'] = x + 'px';
            QS('Desk')['margin-bottom'] = x + 'px';
        }

        // Remote desktop special key combos for Windows
        function deskSendKeys() {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            var ks = Q('deskkeys').value;
            if (ks == 0) { // WIN+Down arrow
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe7, 1], [0xff54, 1], [0xff54, 0], [0xffe7, 0]]); // Intel AMT: Meta-left down, Down arrow press, Down arrow release, Meta-left release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 0x5B], [desktop.m.KeyAction.DOWN, 40], [desktop.m.KeyAction.UP, 40], [desktop.m.KeyAction.EXUP, 0x5B]]); // Agent: L-Winkey press, Down arrow press, Down arrow release, L-Winkey release
                }
            } else if (ks == 1) { // WIN+Up arrow
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe7, 1], [0xff52, 1], [0xff52, 0], [0xffe7, 0]]); // Intel AMT: Meta-left down, Up arrow press, Up arrow release, Meta-left release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 0x5B], [desktop.m.KeyAction.DOWN, 38], [desktop.m.KeyAction.UP, 38], [desktop.m.KeyAction.EXUP, 0x5B]]); // MeshAgent: L-Winkey press, Up arrow press, Up arrow release, L-Winkey release
                }
            } else if (ks == 2) { // WIN+L arrow
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe7, 1], [0x6c, 1], [0x6c, 0], [0xffe7, 0]]); // Intel AMT: Meta-left down, 'l' press, 'l' release, Meta-left release
                } else {
                    desktop.sendCtrlMsg('{"action":"lock"}');
                    //desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN,0x5B],[desktop.m.KeyAction.DOWN,76],[desktop.m.KeyAction.UP,76],[desktop.m.KeyAction.EXUP,0x5B]]); // MeshAgent: L-Winkey press, 'L' press, 'L' release, L-Winkey release
                    //desktop.m.SendKeyMsgKC(desktop.m.KeyAction.EXDOWN, 0x5B);
                    //desktop.m.SendKeyMsgKC(desktop.m.KeyAction.DOWN, 76);
                    //desktop.m.SendKeyMsgKC(desktop.m.KeyAction.UP, 76);
                    //desktop.m.SendKeyMsgKC(desktop.m.KeyAction.EXUP, 0x5B);
                }
            } else if (ks == 3) { // WIN+M arrow
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe7, 1], [0x6d, 1], [0x6d, 0], [0xffe7, 0]]); // Intel AMT: Meta-left down, 'm' press, 'm' release, Meta-left release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 0x5B], [desktop.m.KeyAction.DOWN, 77], [desktop.m.KeyAction.UP, 77], [desktop.m.KeyAction.EXUP, 0x5B]]); // MeshAgent: L-Winkey press, 'M' press, 'M' release, L-Winkey release
                }
            } else if (ks == 4) { // Shift+WIN+M arrow
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe1, 1], [0xffe7, 1], [0x6d, 1], [0x6d, 0], [0xffe7, 0], [0xffe1, 0]]); // Intel AMT: Shift-left down, Meta-left down, 'm' press, 'm' release, Meta-left release, Shift-left release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.DOWN, 16], [desktop.m.KeyAction.EXDOWN, 0x5B], [desktop.m.KeyAction.DOWN, 77], [desktop.m.KeyAction.UP, 77], [desktop.m.KeyAction.EXUP, 0x5B], [desktop.m.KeyAction.UP, 16]]);     // MeshAgent: L-shift press, L-Winkey press, 'M' press, 'M' release, L-Winkey release, L-shift release
                }
            } else if (ks == 5) { // WIN
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe7, 1], [0xffe7, 0]]); // Intel AMT: Meta-left down, Meta-left release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 0x5B], [desktop.m.KeyAction.EXUP, 0x5B]]); // MeshAgent: L-Winkey press, L-Winkey release
                }
            } else if (ks == 6) { // WIN+R
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe7, 1], [0x72, 1], [0x72, 0], [0xffe7, 0]]); // Intel AMT: Meta-left down, 'r' press, 'r' release, Meta-left release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 0x5B], [desktop.m.KeyAction.DOWN, 82], [desktop.m.KeyAction.UP, 82], [desktop.m.KeyAction.EXUP, 0x5B]]); // MeshAgent: L-Winkey press, 'R' press, 'R' release, L-Winkey release
                }
            } else if (ks == 7) { // ALT-F4
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe9, 1], [0xffc1, 1], [0xffc1, 0], [0xffe9, 0]]); // Intel AMT: Alt down, 'F4' press, 'F4' release, Alt release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 18], [desktop.m.KeyAction.DOWN, 115], [desktop.m.KeyAction.UP, 115], [desktop.m.KeyAction.EXUP, 18]]); // MeshAgent: Alt press, 'F4' press, 'F4' release, Alt release
                }
            } else if (ks == 8) { // CTRL-W
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe3, 1], [0x77, 1], [0x77, 0], [0xffe3, 0]]); // Intel AMT: Ctrl down, 'w' press, 'w' release, Ctrl release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 17], [desktop.m.KeyAction.DOWN, 87], [desktop.m.KeyAction.UP, 87], [desktop.m.KeyAction.EXUP, 17]]); // MeshAgent: Ctrl press, 'W' press, 'W' release, Ctrl release
                }
            } else if (ks == 9) { // ALT-TAB
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe9, 1], [0xff09, 1], [0xff09, 0], [0xffe9, 0]]); // Intel AMT: Alt down, 'TAB' press, 'TAB' release, Alt release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.EXDOWN, 18], [desktop.m.KeyAction.DOWN, 9], [desktop.m.KeyAction.UP, 9], [desktop.m.KeyAction.EXUP, 18]]); // MeshAgent: Alt press, 'TAB' press, 'TAB' release, Alt release
                }
            } else if (ks == 10) { // CTRL-ALT-DEL
                desktop.m.sendcad();
            } else if (ks == 11) { // TAB
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xff09, 1], [0xff09, 0]]); // Intel AMT: 'TAB' press, 'TAB' release
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.DOWN, 9], [desktop.m.KeyAction.UP, 9]]); // MeshAgent: 'TAB' press, 'TAB' release
                }
            } else if (ks == 12) { // Shift-F10
                if (desktop.contype == 2) {
                    desktop.m.sendkey([[0xffe1, 1], [0xffc7, 1], [0xffc7, 0], [0xffe1, 0]]); // Intel AMT: Left-Shift Down, F10 down, F10 up, Left-Shift Up
                } else {
                    desktop.m.SendKeyMsgKC([[desktop.m.KeyAction.DOWN, 16], [desktop.m.KeyAction.DOWN, 121], [desktop.m.KeyAction.UP, 121], [desktop.m.KeyAction.UP, 16]]); // MeshAgent: Left-Shift Down, F10 down, F10 up, Left-Shift Up
                }
            }
        }

        function sendSpecialKeys() {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            setDialogMode(3, "특수 키", 3, deskSendKeys);
        }

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

        // Toggle soft keyboard
        function toggleSoftKeys(x) {
            QV('DeskSoftInput', x == 1);
            if (x == 1) { Q('DeskSoftInput').focus(); }
        }

        // Show process dialogs
        function toggleDeskTools() {
            setSessionActivity();
            if (xxdialogMode) return;
            if (QS('DeskTools').display == 'none') {
                QV('DeskTools', true);
                Q('DeskTools').nodeid = currentNode._id;
                refreshDeskTools();
            } else {
                QV('DeskTools', false);
            }
        }

        // Refresh all of the desktop tool panels
        function refreshDeskTools() {
            setSessionActivity();
            QV('DeskToolsRefreshButton', false);
            setTimeout(refreshDeskToolsEx, 500);
            meshserver.send({ action: 'msg', type: 'ps', nodeid: currentNode._id });
        }
        function refreshDeskToolsEx() { QV('DeskToolsRefreshButton', true); }
        var deskTools = { sort: 1, msg: null };
        function sortProcess(sort) { deskTools.sort = sort; showDeskToolsProcesses(deskTools.msg); }
        function sortProcessPid(a, b) { if (a.p > b.p) return 1; if (a.p < b.p) return (-1); return 0; }
        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) { x += '<div class=deskToolsBar><div style=width:50px;float:left;text-align:right;padding-right:5px>' + p[i].p + '</div><a style=float:right;padding-right:5px;cursor:pointer onclick=stopProcess(' + p[i].p + ',"' + p[i].c + '")><img width=10 height=10 src="images/trash.png"></a><div style=float:right;padding-right:5px>' + (p[i].u ? p[i].u : '') + '</div><div>' + p[i].c + '</div></div>'; } }
                QH('DeskToolsProcesses', x);
            }
        }

        // Save the desktop image to file
        function deskSaveImage() {
            setSessionActivity();
            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 deskSelectScreens() {
            if (xxdialogMode || desktop == null || desktop.State != 3) return;
            var x = '', info = desktop.m.displays;
            for (var i in info) { x += '<option value=' + i + ' ' + ((desktop.m.selectedDisplay == i) ? ' selected' : '') + '>' + info[i] + '</option>'; }
            x = addHtmlValue4("Screen", '<select style=width:100% id=deskdisplays>' + x + '</select>');
            setDialogMode(2, "Screen Selection", 3, deskSelectScreensEx, x);
        }

        function deskSelectScreensEx() {
            if (desktop == null || desktop.State != 3) return;
            desktop.m.SetDisplay(parseInt(Q('deskdisplays').value));
        }

        function deskDisplayInfo(sender, info, selDisplay, selItem) {
            var displayCount = 0;
            for (var x in info) { displayCount++; }
            QV('DeskScreens', displayCount > 1);
        }

        function dmousedown(e) { setSessionActivity(); if ((!xxdialogMode && desktop != null)) desktop.m.mousedown(e) }
        function dmouseup(e) { setSessionActivity(); if ((!xxdialogMode && desktop != null)) desktop.m.mouseup(e) }
        function dmousemove(e) { setSessionActivity(); if ((!xxdialogMode && desktop != null)) desktop.m.mousemove(e) }
        function dmousewheel(e) { setSessionActivity(); if ((!xxdialogMode && desktop != null) && 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(); } }
        function stopProcess(id, name) { setDialogMode(2, "프로세스 제어", 3, stopProcessEx, format("프로세스 #{0} \"{1}\"를 중지 하시겠습니까?", id, name), id); return false; }
        function stopProcessEx(buttons, tag) { meshserver.send({ action: 'msg', type: 'pskill', nodeid: currentNode._id, value: tag }); setTimeout(refreshDeskTools, 300); }

        //
        // FILES
        //

        var filesNode;
        function setupFiles() {
            // Setup the files tab
            var samenode = (filesNode == currentNode);
            filesNode = currentNode;
            var online = ((filesNode.conn & 1) != 0) ? true : false; // If Agent (1) connected, enable Terminal
            QE('p13Connect', online);
            if (((samenode == false) || (online == false)) && files) { files.Stop(); files = null; }
        }

        function onFilesStateChange(xfiles, state) {
            setSessionActivity();
            p13Connect.value = (state == 0) ? "연결" : "연결 해제";
            var str = StatusStrs[state];
            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', '');
                    QE('p13FolderUp', false);
                    p13setActions();
                    if (files != null) { files.Stop(); files = null; }
                    break;
                case 3:
                    p13targetpath = '';
                    files.sendText({ action: 'ls', reqid: 1, path: '' });
                    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; } }

        function connectFiles(e) {
            if (!files) {
                // Setup a mesh agent files
                files = CreateAgentRedirect(meshserver, CreateRemoteFiles(p13gotFiles), serverPublicNamePort, authCookie, authRelayCookie, domainUrl);
                files.attemptWebRTC = attemptWebRTC;
                files.onStateChanged = onFilesStateChange;
                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 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);
            data = JSON.parse(decode_utf8(data));
            if (data.action == 'download') { p13gotDownloadCommand(data); return; }

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

            if (data.path != null) {
                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 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 = '<a style=cursor:pointer onclick=p13folderup(0)>' + "최상위폴더" + '</a>', fullPath = 'Root';

            // 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
            for (var i in p13filetreelocation) { displayPath += ' / <a style=cursor:pointer onclick=p13folderup(' + (parseInt(i) + 1) + ')>' + EscapeHtml(p13filetreelocation[i]) + '</a>' } // Setup the path we display
            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;
                shortname = name;
                if (name.length > 70) { shortname = EscapeHtml(name.substring(0, 70)) + "..."; } else { shortname = EscapeHtml(name); }
                name = EscapeHtml(name);

                // Figure out the size
                var fsize = '';
                if (f.s != null) { fsize = getFileSizeStr(f.s); }

                var h = '';
                if (f.t < 3) {
                    var right = '';
                    h = '<div class=filelist file=999><input file=999 style=float:left name=fd class=fcb type=checkbox onchange=p13setActions() value=\'' + f.nx + '\'>&nbsp;<span style=float:right>' + right + '</span><span><div class=fileIcon' + f.t + '></div><a style=cursor:pointer onclick=p13folderset("' + encodeURIComponent(f.nx) + '")>' + shortname + '</a></span></div>';
                } else {
                    var link = shortname;
                    if (f.s > 0) { link = '<a rel=\"noreferrer noopener\" target=\"_blank\" style=cursor:pointer onclick=\"p13downloadfile(\'' + encodeURIComponent(newlinkpath + '/' + name) + '\',\'' + encodeURIComponent(name) + '\',' + f.s + ')\">' + shortname + '</a>'; }
                    h = '<div class=filelist file=3><input file=3 style=float:left name=fd class=fcb type=checkbox onchange=p13setActions() value=\'' + f.nx + '\'>&nbsp;<span style=float:right;padding-right:4px>' + fsize + '</span><span><div class=fileIcon' + f.t + '></div>' + link + '</span></div>';
                }

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

            // Display the files and path
            QH('p13files', html1 + html2);
            QH('p13currentpath', displayPath);
            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 p13folderset(x) {
            p13targetpath = joinPaths(p13filetree.path, p13filetree.dir[x].n).split('\\').join('/');
            files.sendText({ action: 'ls', reqid: 1, path: p13targetpath });
        }

        function p13folderup(x) {
            if (x == null) { p13filetreelocation.pop(); } else { while (p13filetreelocation.length > x) { p13filetreelocation.pop(); } }
            p13targetpath = p13filetreelocation.join('/');
            files.sendText({ action: 'ls', reqid: 1, path: p13targetpath });
        }

        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() {
            if (p13filetree == null) {
                QE('p13DeleteFileButton', false);
                QE('p13NewFolderButton', false);
                QE('p13UploadButton', false);
                QE('p13RenameFileButton', false);
                QE('p13SelectAllButton', false);
                Q('p13SelectAllButton').value = "모두";
                QE('p13RefreshButton', false);
                QE('p13CutButton', false);
                QE('p13CopyButton', false);
                QE('p13PasteButton', 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 = ((currentNode.agent.id > 0) && (currentNode.agent.id < 5));
                QE('p13DeleteFileButton', (cc > 0) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13NewFolderButton', ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13UploadButton', ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13RenameFileButton', (cc == 1) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13SelectAllButton', tc > 0);
                Q('p13SelectAllButton').value = (cc > 0 ? "없음" : "모두");
                QE('p13RefreshButton', true);
                QE('p13CutButton', (cc > 0) && (cc == sfc) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13CopyButton', (cc > 0) && (cc == sfc) && ((p13filetreelocation.length > 0) || (winAgent == false)));
                QE('p13PasteButton', ((p13filetreelocation.length > 0) || (winAgent == false)) && ((p13clipboard != null) && (p13clipboard.length > 0)));
            }
        }

        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 p13selectallfile() { var nv = (p13getFileSelCount() == 0), checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = nv; } p13setActions(); }
        function p13createfolder() { setDialogMode(2, "새 폴더", 3, p13createfolderEx, '<input type=text id=p13renameinput maxlength=64 onkeyup=p13fileNameCheck(event) style=width:100% />'); focusTextBox('p13renameinput'); p13fileNameCheck(); }
        function p13createfolderEx() { files.sendText({ action: 'mkdir', reqid: 1, path: p13filetreelocation.join('/') + '/' + Q('p13renameinput').value }); p13folderup(999); }
        function p13deletefile() { var cc = p13getFileSelCount(), rec = (p13getFileSelDirCount() > 0) ? '<br /><br /><label><input type=checkbox id=p13recdeleteinput>' + "재귀 삭제" + '</label><br>' : '<input type=checkbox id=p13recdeleteinput style=\'display:none\'>'; setDialogMode(2, "삭제", 3, p13deletefileEx, (cc > 1) ? (format("선택한 항목 {0}을 삭제하시겠습니까?", cc) + rec) : ("선택한 항목을 삭제 하시겠습니까?" + rec)); }
        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; } } setDialogMode(2, "이름 바꾸기", 3, p13renamefileEx, '<input type=text id=p13renameinput maxlength=64 onkeyup=p13fileNameCheck(event) style=width:100% value="' + renamefile + '" />', { 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() { setDialogMode(2, "파일 업로드", 3, p13uploadFileEx, '<input type=file name=files id=p13uploadinput style=width:100% multiple=multiple onchange="updateUploadDialogOk(\'p13uploadinput\')" />'); updateUploadDialogOk('p13uploadinput'); }
        function p13uploadFileEx() { p13doUploadFiles(Q('p13uploadinput').files); }
        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(encodeURIComponent(p13filetreelocation.join('/') + '/' + p13filetree.dir[checkboxes[i].value].n), encodeURIComponent(p13filetree.dir[checkboxes[i].value].n), p13filetree.dir[checkboxes[i].value].s, 'viewer');
                    } else { messagebox("파일 편집기", "200k 미만의 파일만 편집 할 수 있습니다."); }
                    break;
                }
            }
        }

        var p13clipboard = null, p13clipboardFolder = null, p13clipboardCut = 0;
        function p13copyFile(cut) { var checkboxes = document.getElementsByName('fd'); p13clipboard = []; p13clipboardCut = cut, p13clipboardFolder = p13targetpath; for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && (checkboxes[i].attributes.file.value == '3')) { p13clipboard.push(p13filetree.dir[checkboxes[i].value].n); } } p13updateClipview(); }
        function p13pasteFile() {
            var x = '';
            if ((p13clipboard != null) && (p13clipboard.length > 0)) {
                if (p13clipboardCut == 0) {
                    if (p13clipboard.length > 1) { x = format("{0} 항목을 이 위치로 복사하시겠습니까?", p13clipboard.length); } else { x = format("1개의 항목을 이 위치로 복사하시겠습니까?"); }
                } else {
                    if (p13clipboard.length > 1) { x = format("{0} 항목들을 이 위치로 이동하시겠습니까?", p13clipboard.length); } else { x = format("이 위치로 1개 항목을 이동하시겠습니까?"); }
                }
            }
            setDialogMode(2, "붙여넣기", 3, p13pasteFileEx, x);
        }
        function p13pasteFileEx() { files.sendText({ action: (p13clipboardCut == 0 ? 'copy' : 'move'), reqid: 1, scpath: p13clipboardFolder, dspath: p13targetpath, names: p13clipboard }); p13folderup(999); if (p13clipboardCut == 1) { p13clipboard = null, p13clipboardFolder = null, p13clipboardCut = 0; p13updateClipview(); } }
        function p13updateClipview() {
            var x = '';
            if ((p13clipboard != null) && (p13clipboard.length > 0)) {
                if (p13clipboardCut == 0) {
                    if (p13clipboard.length > 1) {
                        x = format("복사할 {0} 항목 보유" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "지우기" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("1개 복사 항목 보유" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "지우기" + '</a>.');
                    }
                } else {
                    if (p13clipboard.length > 1) {
                        x = format("이동할 {0} 항목 보유" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "지우기" + '</a>.', p13clipboard.length);
                    } else {
                        x = format("1개 이동 항목 보유" + ', <a href=# onclick="return p13clearClip()" style=cursor:pointer>' + "지우기" + '</a>.');
                    }
                }
            }
            QH('p13bottomstatus', x);
            p13setActions();
        }
        function p13clearClip() { p13clipboard = null; p13clipboardFolder = null; p13clipboardCut = 0; p13updateClipview(); return false; } function updateUploadDialogOk(x) { QE('idx_dlgOkButton', Q(x).value != ''); }
        function getFileSelCount(includeDirs) { var cc = 0; var 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 getFileCount() { var cc = 0; var checkboxes = document.getElementsByName('fc'); return checkboxes.length; }

        //
        // FILES DOWNLOAD
        //

        var downloadFile; // 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) {
            if (xxdialogMode || downloadFile || !files) return;
            downloadFile = { path: decodeURIComponent(x), file: decodeURIComponent(y), size: z, tsize: 0, data: '', state: 0, id: Math.random() }
            //console.log('p13downloadFileCancel', downloadFile);
            files.sendText({ action: 'download', sub: 'start', id: downloadFile.id, path: downloadFile.path });
            setDialogMode(2, "파일 다운로드", 10, p13downloadFileCancel, '<div>' + downloadFile.file + '</div><br /><progress id=d2progressBar style=width:100% value=0 max=' + z + ' />');
        }

        // Called by the html page to cancel the download
        function p13downloadFileCancel() { setDialogMode(0); files.sendText({ action: 'download', sub: 'cancel', id: downloadFile.id }); downloadFile = null; }

        // Called by the transport when download control command is received
        function p13gotDownloadCommand(cmd) {
            //console.log('p13gotDownloadCommand', cmd);
            if ((downloadFile == null) || (cmd.id != downloadFile.id)) return;
            if (cmd.sub == 'start') { downloadFile.state = 1; files.sendText({ action: 'download', sub: 'startack', id: downloadFile.id }); }
            else if (cmd.sub == 'cancel') { downloadFile = null; setDialogMode(0); }
        }

        // Called by the transport when binary data is received
        function p13gotDownloadBinaryData(data) {
            if (!downloadFile || downloadFile.state == 0) return;
            if (data.length > 4) {
                downloadFile.tsize += (data.length - 4); // Add to the total bytes received
                downloadFile.data += data.substring(4); // Append the data
                Q('d2progressBar').value = downloadFile.tsize; // Change the progress bar
            }
            if ((ReadInt(data, 0) & 1) != 0) { // Check end flag
                saveAs(data2blob(downloadFile.data), downloadFile.file); downloadFile = null; setDialogMode(0); // Save the file
            } else {
                files.sendText({ action: 'download', sub: 'ack', id: downloadFile.id }); // Send the ACK
            }
        }

        /*
        var downloadFile; // 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) {
            if (xxdialogMode) return;
            downloadFile = CreateAgentRedirect(meshserver, CreateRemoteFiles(p13gotDownloadData), serverPublicNamePort, authCookie, authRelayCookie, domainUrl); // Create our websocket file transport
            downloadFile.ctrlMsgAllowed = false;
            downloadFile.onStateChanged = onFileDownloadStateChange;
            downloadFile.xpath = decodeURIComponent(x);
            downloadFile.xfile = decodeURIComponent(y);
            downloadFile.xsize = z;
            downloadFile.xtsize = 0;
            downloadFile.xstate = 0;
            downloadFile.Start(filesNode._id);
            setDialogMode(2, "Download File", 10, p13downloadFileCancel, '<div>' + downloadFile.xfile + '</div><br /><progress id=d2progressBar style=width:100% value=0 max=' + z + ' />');
        }

        // Called by the html page to cancel the download
        function p13downloadFileCancel(button, tag) {
            //console.log('p13downloadFileCancel');
            downloadFile.Stop();
            delete downloadFile;
            downloadFile = null;
        }

        // Called by the file transport to indicate when the transport connection state has changed
        function onFileDownloadStateChange(xdownloadFile, state) {
            switch (state) {
                case 0: // Transport as disconnected. If this is not part of an abort, we need to save the file
                    setDialogMode(0); // Close any dialog boxes if present
                    if ((downloadFile != null) && (downloadFile.xstate == 1)) { saveAs(data2blob(downloadFile.xdata), downloadFile.xfile); } // Save the file
                    break;
                case 3: // Transport as connected, send a command to indicate we want to start a file download
                    downloadFile.send(JSON.stringify({ action: 'download', reqid: 1, path: downloadFile.xpath }));
                    break;
                default:
                    console.log('Unknown onFileDownloadStateChange state', state);
                    break;
            }
        }

        // Called by the transport when data is received
        function p13gotDownloadData(data) {
            if (downloadFile.xstate == 0) { // If state is 0, this is a command confirming if the file will be transfered.
                var cmd = JSON.parse(data);
                if (cmd.action == 'downloadstart') { // Yes, the file is about to start
                    downloadFile.xstate = 1; // Switch to state 1, we will start receiving the file data
                    downloadFile.xdata = ''; // Start with empty data
                    downloadFile.send('a'); // Send the first ACK
                } else if (cmd.action == 'downloaderror') { // Problem opening this file, cancel
                    p13downloadFileCancel();
                }
            } else { // We are in the process of receiving the file
                downloadFile.xtsize += (data.length); // Add to the total bytes received
                downloadFile.xdata += data; // Append the data
                Q('d2progressBar').value = downloadFile.xtsize; // Change the progress bar
                downloadFile.send('a'); // Send the ACK
            }
        }
        */

        //
        // FILES UPLOAD
        //

        var uploadFile;
        function p13doUploadFiles(files) {
            if (xxdialogMode) return;

            // Check if we are going to overwrite any files
            var winAgent = ((currentNode.agent.id > 0) && (currentNode.agent.id < 5));
            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
                setDialogMode(2, "파일 업로드", 3, p13uploadFileContinue, format((overWriteCount == 1) ? "업로드하면 파일 1개를 덮어씁니다. 계속할까요?" : "업로드하면 {0} 파일을 덮어씁니다. 계속할까요?", overWriteCount), files);
            }
        }

        function p13uploadFileContinue(b, files) {
            uploadFile = {};
            uploadFile.xpath = p13filetreelocation.join('/');
            uploadFile.xfiles = files;
            uploadFile.xfilePtr = -1;
            setDialogMode(2, "파일 업로드", 10, p13uploadFileCancel, '<div id=p13dfileName>' + "연결 중 ..." + '</div><br /><progress id=d2progressBar style=width:100% value=0 max=0 />');
            p13uploadNextFile();
        }

        // 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', file.name);
                Q('d2progressBar').max = file.size;
                Q('d2progressBar').value = 0;
                if (file.xdata == null) {
                    // Load the data
                    uploadFile.xreader = new FileReader();
                    uploadFile.xreader.onload = function () {
                        uploadFile.xdata = uploadFile.xreader.result;
                        files.sendText(JSON.stringify({ action: 'upload', reqid: uploadFile.xfilePtr, path: uploadFile.xpath, name: file.name, size: uploadFile.xdata.byteLength }));
                    };
                    uploadFile.xreader.readAsArrayBuffer(file);
                } 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': { p13uploadNextPart(false); for (var i = 0; i < 8; i++) { p13uploadNextPart(true); } 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; }
            }
        }

        // 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) {
            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 + 16384;
                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;
            }
        }

        //
        // DEVICE DETAILS
        //

        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
            if ((hardware.windows && hardware.windows.osinfo) || node.osdesc) {
                var x = '';
                if (node.rname) { x += addDetailItem("이름", EscapeHtml(node.rname), s); }
                if (node.osdesc) { x += addDetailItem("버전", EscapeHtml(node.osdesc), s); }
                if (hardware.windows && hardware.windows.osinfo) {
                    var m = hardware.windows.osinfo;
                    if (m.OSArchitecture) { x += addDetailItem("구조", EscapeHtml(m.OSArchitecture), s); }
                }
                if (x != '') { sections.push({ name: "운영체제", html: x, img: 'software32.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; }
                    x += addDetailItem("메시 에이전트", str);
                }
                if ((node.conn & 1) != 0) {
                    x += addDetailItem("마지막 에이전트 연결", "지금 연결됨");
                } else {
                    if (node.lastconnect) { x += addDetailItem("마지막 에이전트 연결", printDateTime(new Date(node.lastconnect))); }
                }
                if (node.lastaddr) {
                    var splitip = node.lastaddr.split(':');
                    if (splitip.length > 2) {
                        // IPv6
                        x += addDetailItem("마지막 에이전트 주소", node.lastaddr);
                    } else {
                        // IPv4
                        if (isPrivateIP(node.lastaddr)) {
                            x += addDetailItem("마지막 에이전트 주소", splitip[0]);
                        } else {
                            x += addDetailItem("마지막 에이전트 주소", '<a href="https://iplocation.com/?ip=' + splitip[0] + '" rel="noreferrer noopener" target="MeshIPLoopup">' + splitip[0] + '</a>');
                        }
                    }
                }
                if (x != '') { sections.push({ name: "메시 에이전트", html: x, img: 'meshagent32.png' }); }
            }

            // Networking
            if (network.netif2 != null) {
                // Display one network interface for each MAC address
                var x = '';
                x += '<table 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>';
                    x += '<div style=margin-bottom:3px><b>' + EscapeHtml(i + (m[0].fqdn ? (', ' + m[0].fqdn) : '')) + '</b></div>';
                    if (m.desc) { x += addDetailItem("설명", EscapeHtml(m.desc).split('(R)').join('&reg;')); }
                    //if (m.dnssuffix) { x += addDetailItem("DNS Suffix", m.dnssuffix); }
                    if (typeof m[0].mac == 'string') {
                        if (m[0].gatewaymac) {
                            x += addDetailItem("MAC 계층", format("MAC : {0}, 게이트웨이 : {1}", EscapeHtml(m[0].mac), EscapeHtml(m[0].gatewaymac)));
                        } else {
                            x += addDetailItem("MAC 계층", format("MAC : {0}", EscapeHtml(m[0].mac)));
                        }
                    }
                    for (var j = 0; j < m.length; j++) {
                        var iplayer = m[j];
                        if (iplayer.family == 'IPv4') {
                            if (iplayer.gateway && iplayer.netmask) {
                                x += addDetailItem("IPv4 레이어", format("IP : {0}, 마스크 : {1}, 게이트웨이 : {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
                            } else {
                                x += addDetailItem("IPv4 레이어", format("IP : {0}", EscapeHtml(iplayer.address)));
                            }
                        }
                        if (iplayer.family == 'IPv6') {
                            if (iplayer.gateway && iplayer.netmask) {
                                x += addDetailItem("IPv6 Layer", format("IP : {0}, 마스크 : {1}, 게이트웨이 : {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
                            } else {
                                x += addDetailItem("IPv6 Layer", format("IP : {0}", EscapeHtml(iplayer.address)));
                            }
                        }
                    }
                    x += '</div>';
                }
                x += '</table>';
                if (x != '') { sections.push({ name: "네트워킹", html: x, img: 'networking64.png' }); }
            }

            // Attribute: Intel AMT
            if (node.intelamt != null) {
                var x = '';
                x += addDetailItem("버전", (node.intelamt.ver) ? ('v' + EscapeHtml(node.intelamt.ver)) : ('<i>' + "알 수 없는" + '</i>'), s);
                var provisioningStates = { 0: nobreak("활성화되지 않음 (Pre)"), 1: nobreak("활성화되지 않음 (In)"), 2: nobreak("활성화 됨") };
                var provisioningMode = '';
                if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "클라이언트 제어 모드 (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "관리 제어 모드 (ACM)"); } }
                x += addDetailItem("프로비저닝 상태", ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('<i>' + "알 수 없는" + '</i>')) + provisioningMode, s);
                x += addDetailItem("보안", (node.intelamt.tls == 1) ? "TLS 보안" : "TLS가 설정되지 않았습니다", s);
                x += addDetailItem("관리자 자격 증명", (node.intelamt.user == null || node.intelamt.user == '') ? "알려지지 않음" : "알려진", s);
                if (x != '') { sections.push({ name: "Intel&reg; Active Management Technology (Intel&reg; AMT)", html: x, img: 'amt32.png' }); }
            }

            if (hardware.identifiers) {
                var x = '', ident = hardware.identifiers;
                // BIOS
                if (ident.bios_vendor) { x += addDetailItem("공급업체", EscapeHtml(ident.bios_vendor), s); }
                if (ident.bios_version) { x += addDetailItem("버전", EscapeHtml(ident.bios_version), s); }
                if (x != '') { sections.push({ name: "BIOS", html: x, img: 'chip32.png' }); }

                // Motherboard
                x = '';
                if (ident.board_vendor) { x += addDetailItem("공급업체", EscapeHtml(ident.board_vendor), s); }
                if (ident.board_name) { x += addDetailItem("이름", EscapeHtml(ident.board_name), s); }
                if (ident.board_serial && (ident.board_serial != '')) { x += addDetailItem("일련번호", EscapeHtml(ident.board_serial), s); }
                if (ident.board_version) { x += addDetailItem("버전", EscapeHtml(ident.board_version), s); }
                if (ident.product_uuid) { x += addDetailItem("식별자", EscapeHtml(ident.product_uuid), s); }
                if (ident.cpu_name) { x += addDetailItem("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: "메인보드", html: x, img: 'motherboard32.png' }); }
            }

            if (hardware.windows) {
                if (hardware.windows.memory) {
                    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 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) + '</b></div>';
                        if (m.Capacity) { x += addDetailItem("용량 / 속도", format("{0} Mb, {1} Mhz", (m.Capacity / 1024 / 1024), m.Speed), s); }
                        if (m.PartNumber) { x += addDetailItem("부품번호", EscapeHtml((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + EscapeHtml(m.PartNumber), s); }
                        x += '</div>';
                    }
                    x += '</table>';

                    if (x != '') { sections.push({ name: "메모리", html: x, img: 'ram32.png' }); }
                }
            }

            // Storage
            if (hardware.identifiers && ident.storage_devices) {
                var x = '';
                // Sort Storage
                ident.storage_devices.sort(function (a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });

                x += '<table style=width:100%>';
                for (var i in ident.storage_devices) {
                    var m = ident.storage_devices[i];
                    if (m.Size) {
                        x += '<tr><td><div class=style10 style=border-radius:5px;padding:8px>';
                        x += '<div style=margin-bottom:3px><b>' + EscapeHtml(m.Caption) + '</b></div>';
                        if (m.Model && (m.Model != m.Caption)) { x += addDetailItem("모델", EscapeHtml(m.Model), s); }
                        if (m.Size) {
                            if ((typeof m.Size == 'string') && (parseInt(m.Size) == m.Size)) { m.Size = parseInt(m.Size); }
                            if (typeof m.Size == 'number') { x += addDetailItem("용량", format("{0} Mb", Math.floor(m.Size / 1024 / 1024)), s); }
                            if (typeof m.Size == 'string') { x += addDetailItem("용량", EscapeHtml(m.Size), s); }
                        }
                        x += '</div>';
                    }
                }
                x += '</table>';

                if (x != '') { sections.push({ name: "저장소", html: x, img: 'storage32.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:4px><b>' + sections[i].name + '</b></div><div style=margin-bottom:10px;margin-left:4px>' + sections[i].html + '</div>';
                } else {
                    x += '<table style=width:100%><tr>';
                    x += '<td style=width:32px;vertical-align:top><img src=images/details/' + sections[i].img + ' border=0 width=32 /></td>'; // height=12 
                    x += '<td><div class=DevSt style=margin-bottom:3px;margin-left:4px><b>' + sections[i].name + '</b></div><div style=margin-bottom:10px;margin-left:4px>' + sections[i].html + '</div></td>';
                    x += '</tr></table>';
                }
            }

            if (x == '') {
                QH('p10detailshtml', "이 장치에 대한 정보가 없습니다.");
            } else {
                QH('p10detailshtml', x);
            }
        }

        //
        // MY MESHS
        //

        var currentMesh;
        function p20updateMesh() {
            if (currentMesh == null) return;
            QH('p20meshName', EscapeHtml(currentMesh.name));
            var meshtype = format("알 수 없는 # {0}", currentMesh.mtype);
            var meshrights = GetMeshRights(currentMesh);
            if (currentMesh.mtype == 1) meshtype = "오직 Intel&reg; AMT만, 에이전트 없음";
            if (currentMesh.mtype == 2) meshtype = "소프트웨어 에이전트로 관리됨";

            var x = '';
            x += addHtmlValue("이름", addLinkConditional(EscapeHtml(currentMesh.name), 'p20editmesh(1)', (meshrights & 1) != 0));
            x += addHtmlValue("설명", addLinkConditional(((currentMesh.desc && currentMesh.desc != '') ? EscapeHtml(currentMesh.desc) : ('<i>' + "없음" + '</i>')), 'p20editmesh(2)', (meshrights & 1) != 0));
            x += addHtmlValue("유형", meshtype);
            //x += addHtmlValue('Identifier', currentMesh._id.split('/')[2]);

            //x += '<br><input type=button value=Notes onclick=showNotes(false,"' + encodeURIComponent(currentMesh._id) + '") />';

            x += '<br style=clear:both><br>';
            var currentMeshLinks = currentMesh.links[userinfo._id];
            if (currentMeshLinks && ((currentMeshLinks.rights & 2) != 0)) { x += '<div style=margin-bottom:6px><a onclick=p20showAddMeshUserDialog() style=cursor:pointer><img src=images/icon-addnew.png border=0 height=12 width=12> ' + "사용자 추가" + '</a></div>'; }

            /*
            if ((meshrights & 4) != 0) {
                if (currentMesh.mtype == 1) {
                    x += '<a onclick=addCiraDeviceToMesh("' + currentMesh._id + '") style=cursor:pointer;margin-right:10px><img src=images/icon-installmesh.png border=0 height=12 width=12> Install CIRA</a>';
                    x += '<a onclick=addDeviceToMesh("' + currentMesh._id + '") style=cursor:pointer;margin-right:10px><img src=images/icon-installmesh.png border=0 height=12 width=12> Install local</a>';
                }
                if (currentMesh.mtype == 2) {
                    x += '<a onclick=addAgentToMesh("' + currentMesh._id + '") style=cursor:pointer;margin-right:10px><img src=images/icon-addnew.png border=0 height=12 width=12> Install</a>';
                }
            }
            */

            /*
            function getMeshActions(mesh, meshrights) {
                if ((meshrights & 4) == 0) return '';
                var r = '';
                if (mesh.mtype == 1) {
                    r += ' <a style=cursor:pointer;font-size:10px onclick=addCiraDeviceToMesh("' + mesh._id + '")>Add CIRA</a>';
                    r += ' <a style=cursor:pointer;font-size:10px onclick=addDeviceToMesh("' + mesh._id + '")>Add Local</a>';
                }
                if (mesh.mtype == 2) {
                    r += ' <a style=cursor:pointer;font-size:10px onclick=addAgentToMesh("' + mesh._id + '")>Add Agent</a>';
                }
                return r;
            }
            */

            x += '<table style="color:black;background-color:#EEE;border-color:#AAA;border-width:1px;border-style:solid;border-collapse:collapse" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col style=text-align:left;width:430px>' + "사용자 인증" + '</th></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 mesh
            for (var i in sortedusers) {
                var trash = '', rights = "부분적인 권리", r = sortedusers[i].rights, icon = 2;
                if (r == 0xFFFFFFFF) rights = "전체 관리자"; else if (r == 0) rights = "권한이 없습니다.";
                if ((i != userinfo._id) && (meshrights == 0xFFFFFFFF || (((meshrights & 2) != 0)))) { trash = '<a onclick=p20deleteUser(event,"' + encodeURIComponent(sortedusers[i].id) + '") style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>'; }
                if (sortedusers[i].id.startsWith('ugrp/')) { icon = 4; }
                x += '<tr onclick=p20viewuser("' + encodeURIComponent(sortedusers[i].id) + '") style=height:32px;cursor:pointer' + (((count % 2) == 0) ? ';background-color:#DDD' : '') + '><td>';
                x += '<div style=float:right>' + trash + '</div><div style=float:right;padding-right:4px>' + rights + '</div><div class=m' + icon + '></div><div>&nbsp;' + EscapeHtml(decodeURIComponent(sortedusers[i].name)) + '<div></div></div>';
                x += '</td></tr>';
                ++count;
            }

            x += '</tbody></table>';

            // If we are full administrator on this mesh, allow deletion of the mesh
            if (meshrights == 0xFFFFFFFF) { x += '<div style=font-size:small;text-align:right;margin-top:6px><span><a onclick=p20showDeleteMeshDialog() style=cursor:pointer>' + "그룹 삭제" + '</a></span></div>'; }

            QH('p20info', x);
        }

        function p20showDeleteMeshDialog() {
            if (xxdialogMode) return false;
            var x = format("그룹 {0}을(를) 삭제 하시겠습니까? 장치 그룹을 삭제하면 이 그룹 내 장치에 대한 모든 정보도 함께 삭제됩니다.", EscapeHtml(currentMesh.name)) + '<br /><br />';
            x += '<label><input id=p20check type=checkbox onchange=p20validateDeleteMeshDialog() />' + "확인" + '</label>';
            setDialogMode(2, "그룹 삭제", 3, p20showDeleteMeshDialogEx, x);
            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 p20editmesh(focus) {
            if (xxdialogMode) return;
            var x = addHtmlValue("이름", '<input id=dp20meshname style=width:170px maxlength=32 onchange=p20editmeshValidate() onkeyup=p20editmeshValidate() />');
            x += addHtmlValue("설명", '<input id=dp20meshdesc style=width:170px maxlength=1024 onkeyup=p20editmeshValidate() />');
            setDialogMode(2, "장치 그룹 편집", 3, p20editmeshEx, x);
            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() {
            QE('idx_dlgOkButton', Q('dp20meshname').value.length > 0);
        }

        function p20showAddMeshUserDialog() {
            if (xxdialogMode) return;
            var x = addHtmlValue('User ID', '<input id=dp20username style=width:170px maxlength=256 onchange=p20validateAddMeshUserDialog() onkeyup=p20validateAddMeshUserDialog() />');
            x += '<div style="border:2px groove gray;background-color:white;max-height:120px;overflow-y:scroll">';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20fulladmin>' + "전체 관리자" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20editmesh>' + "장치 그룹 편집" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20manageusers>' + "장치 그룹 사용자 관리" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20managecomputers>' + "장치 그룹 컴퓨터 관리" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20remotecontrol>' + "원격 제어" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20remoteview style=margin-left:12px>' + "오직 원격 보기만" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20remotelimitedinput style=margin-left:12px>' + "오직 제한된 입력만" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20noterminal style=margin-left:12px>' + "접근 가능한 터미널이 없습니다." + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20nofiles style=margin-left:12px>' + "접근할 수 있는 파일이 없습니다." + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20noamt style=margin-left:12px>' + "Intel&reg; AMT 없음" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20meshagentconsole>' + "메시 에이전트 콘솔" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20meshserverfiles>' + "서버 파일" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20wakedevices>' + "장치 깨우기(Wake)" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20editnotes>' + "장치 메모 편집" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20limitevents>' + "오직 자신의 이벤트만 표시" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20chatnotify>' + "채팅 및 알림" + '</label><br>';
            x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20uninstall>' + "에이전트 제거" + '</label><br>';
            x += '</div>';
            setDialogMode(2, "사용자를 장치 그룹에 추가", 3, p20showAddMeshUserDialogEx, x);
            p20validateAddMeshUserDialog();
            Q('dp20username').focus();
        }

        function p20validateAddMeshUserDialog() {
            var meshrights = GetMeshRights(currentMesh);
            var nc = !Q('p20fulladmin').checked;
            QE('p20fulladmin', meshrights == 0xFFFFFFFF);
            QE('p20editmesh', nc && (meshrights == 0xFFFFFFFF));
            QE('p20manageusers', nc);
            QE('p20managecomputers', nc);
            QE('p20remotecontrol', nc);
            QE('p20meshagentconsole', nc);
            QE('p20meshserverfiles', nc);
            QE('p20wakedevices', nc);
            QE('p20editnotes', nc);
            QE('p20limitevents', nc);
            QE('p20remoteview', nc && Q('p20remotecontrol').checked);
            QE('p20remotelimitedinput', nc && Q('p20remotecontrol').checked && !Q('p20remoteview').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);
        }

        function p20showAddMeshUserDialogEx() {
            var meshadmin = 0;
            if (Q('p20fulladmin').checked == true) { meshadmin = 0xFFFFFFFF; } else {
                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('p20noterminal').checked == true) meshadmin += 512;
                if (Q('p20nofiles').checked == true) meshadmin += 1024;
                if (Q('p20noamt').checked == true) meshadmin += 2048;
                if (Q('p20remotelimitedinput').checked == true) meshadmin += 4096;
                if (Q('p20limitevents').checked == true) meshadmin += 8192;
                if (Q('p20chatnotify').checked == true) meshadmin += 16384;
                if (Q('p20uninstall').checked == true) meshadmin += 32768;
            }
            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 });
        }

        function p20viewuser(userid) {
            if (xxdialogMode) return;
            userid = decodeURIComponent(userid);
            var r = [], cmeshrights = GetMeshRights(currentMesh), meshrights = GetMeshRights(currentMesh, userid);
            if (meshrights == 0xFFFFFFFF) r.push("전체 관리자"); else {
                if ((meshrights & 1) != 0) r.push("장치 그룹 편집");
                if ((meshrights & 2) != 0) r.push("장치 그룹 사용자 관리");
                if ((meshrights & 4) != 0) r.push("장치 그룹 컴퓨터 관리");
                if ((meshrights & 8) != 0) r.push("원격 제어");
                if ((meshrights & 16) != 0) r.push("에이전트 콘솔");
                if ((meshrights & 32) != 0) r.push("서버 파일");
                if ((meshrights & 64) != 0) r.push("장치 깨우기(Wake)");
                if ((meshrights & 128) != 0) r.push("메모 편집");
                if ((meshrights & 256) != 0) r.push("오직 원격 보기만");
                if ((meshrights & 512) != 0) r.push("터미널이 없습니다.");
                if ((meshrights & 1024) != 0) r.push("파일이 없습니다.");
                if ((meshrights & 2048) != 0) r.push("Intel&reg; AMT 없음");
                if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("제한된 입력");
                if ((meshrights & 8192) != 0) r.push("오직 자체 이벤트만");
                if ((meshrights & 16384) != 0) r.push("채팅 및 알림");
                if ((meshrights & 32768) != 0) r.push("설치 제거");
            }
            if (r.length == 0) { r.push("권한이 없습니다."); }
            var buttons = 1, uname = userid.split('/')[2];
            if (currentMesh.links[userid].name) { uname = currentMesh.links[userid].name; }
            var x = addHtmlValue("사용자 이름", EscapeHtml(uname));
            if (uname != userid.split('/')[2]) { x += addHtmlValue("User ID", EscapeHtml(userid.split('/')[2])); }
            x += addHtmlValue("권한", r.join(", "));
            if (((userinfo._id) != userid) && (cmeshrights == 0xFFFFFFFF || (((cmeshrights & 2) != 0) && (meshrights != 0xFFFFFFFF)))) buttons += 4;
            setDialogMode(2, "장치 그룹 사용자", buttons, p20viewuserEx, x, userid);
        }

        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; }
            setDialogMode(2, "원격 메시 사용자", 3, p20viewuserEx2, format("사용자 {0}를 삭제하시겠습니까?", uname), userid);
        }
        function p20deleteUser(e, userid) { haltEvent(e); p20viewuserEx(2, decodeURIComponent(userid)); }
        function p20viewuserEx2(button, userid) { meshserver.send({ action: 'removemeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userid: userid }); }

        //
        // PANELS
        //

        var xxcurrentView = -1;
        function go(x) {
            setSessionActivity();
            if (xxdialogMode || xxcurrentView == x) return;
            updateFooterMenu();
            setDialogMode(0);
            // Edit this line when adding a new screen
            for (var i = 0; i < 32; i++) { QV('p' + i, i == x); }
            xxcurrentView = x;
        }

        //
        // POPUP DIALOG
        //

        // undefined = Hidden, 1 = Generic Message
        var xxdialogMode;
        var xxdialogFunc;
        var xxdialogButtons;
        var xxdialogTag;

        // 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();
            xxdialogMode = x;
            xxdialogFunc = f;
            xxdialogButtons = b;
            xxdialogTag = tag;
            QE('idx_dlgOkButton', true);
            QV('idx_dlgOkButton', b & 1);
            QV('idx_dlgCancelButton', b & 2);
            QV('id_dialogclose', (b & 2) || (b & 8));
            QV('idx_dlgButtonBar', b & 7);
            if (y) QH('id_dialogtitle', 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); } }
        }

        function dialogclose(x) {
            setSessionActivity();
            var f = xxdialogFunc;
            var b = xxdialogButtons;
            var t = xxdialogTag;
            setDialogMode();
            if (((b & 8) || x) && f) f(x, t);
        }

        //
        // Access Control Functions
        // These must match server
        //

        // 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 (userinfo.manageAllDeviceGroups && (userid == userinfo._id)) return 0xFFFFFFFF;

            // Check device group link permission
            var rights = 0, r = mesh.links[userid];
            if (r != null) {
                if (r.rights == 0xFFFFFFFF) { return 0xFFFFFFFF; } // 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 0xFFFFFFFF; } // User has full rights thru a user group, stop here.
                            rights |= r.rights; // TODO: Deal with reverse permissions
                        }
                    }
                }
            }

            return rights;
        }

        // 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 (userinfo.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 r;
            var user = null;

            // 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
            /*
            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 r;
        }

        // 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
        //

        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 (e) { } if (name[0] != '_') { var s = {}; for (var i = 0, len = localStorage.length; i < len; ++i) { var k = localStorage.key(i); if (k[0] != '_') { s[k] = localStorage.getItem(k); } } meshserver.send({ action: 'userWebState', state: JSON.stringify(s) }); } }
        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 center() { onDevicesScroll(); QS('dialog').left = ((((getDocWidth() - 300) / 2)) + 'px'); deskAdjust(); deskAdjust(); /*drawDeviceTimeline();*/ }
        function messagebox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t, 1); }
        function statusbox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t); }
        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 haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
        function haltReturn(e) { if (e.keyCode == 13) { haltEvent(e); } }
        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); }
        function reload() { window.location.href = window.location.href; }
        function getNodeFromId(id) { for (var i in nodes) { if (nodes[i]._id == id) return nodes[i]; } return null; }
        function addHtmlValue(t, v) { return '<table><td style=width:120px>' + t + '<td><b>' + v + '</b></table>'; }
        function addHtmlValue2(t, v) { return '<div><div style=display:inline-block;float:right>' + v + '</div><div style=display:inline-block>' + t + '</div></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 addLink(x, f) { return '<a style=cursor:pointer;color:darkblue;text-decoration:none onclick=\'' + f + '\'>&diams; ' + x + '</a>'; }
        function addLinkConditional(x, f, c) { if (c) return addLink(x, f); return x; }
        function passwordcheck(p) { var re = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()]).{8,}/; return re.test(p); }
        function getFileSizeStr(size) { if (typeof size != 'number') { size = 0; } if (size == 1) return "1바이트"; return format('{0} bytes', size); }
        function joinPaths() { var x = []; for (var i in arguments) { var w = arguments[i]; if ((w != null) && (w != '')) { while (w.endsWith('/') || w.endsWith('\\')) { w = w.substring(0, w.length - 1); } while (w.startsWith('/') || w.startsWith('\\')) { w = w.substring(1); } x.push(w); } } return x.join('/'); }
        function focusTextBox(x) { setTimeout(function () { Q(x).selectionStart = Q(x).selectionEnd = 65535; Q(x).focus(); }, 0); }
        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 printDate(d) { return d.toLocaleDateString(args.locale); }
        function printTime(d) { return d.toLocaleTimeString(args.locale); }
        function printDateTime(d) { return d.toLocaleString(args.locale); }
        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 nobreak(x) { return x.split(' ').join('&nbsp;'); }
        function getUserName(userid) { if (users && users[userid] != null) return users[userid].name; return userid.split('/')[2]; }
        function addDetailItem(title, value, state) { return '<table style=width:100%><td>' + nobreak(title) + '<td style=text-align:right>' + value + '</table>'; }

    </script>

</body></html>