Files
netdesk/host/netdesk_gui.html

972 lines
60 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NetDesk v1.5.0 — Enterprise Remote Desktop</title>
<style>
:root {
--win-bg: #11161d; --sidebar-bg: #0d1117; --card-bg: #181f29; --card-border: #26303f;
--input-bg: #0f141b; --input-border: #252f3d;
--em: #19c87e; --em-br: #25db8e; --crimson-bg: #321c22; --crimson-bd: #7c3139; --crimson-fg: #e05a61;
--blue: #3b9eff; --fg: #e6edf3; --muted: #8e9aa9; --faint: #64748b;
}
* { box-sizing: border-box; user-select: none; -webkit-font-smoothing: antialiased; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
background: radial-gradient(circle at 18% 15%, rgba(25,200,126,.08) 0%, transparent 42%),
radial-gradient(circle at 82% 85%, rgba(59,158,255,.07) 0%, transparent 45%),
linear-gradient(180deg, #0b0f15 0%, #06080c 100%);
font-family: 'Segoe UI', 'Inter', system-ui, sans-serif; color: var(--fg); font-size: 13.5px;
overflow: hidden;
}
.mono { font-family: 'Cascadia Mono', Consolas, monospace; user-select: text; }
input, textarea { user-select: text; }
.shell { height: 100vh; display: flex; flex-direction: column; background: var(--win-bg);
border: 1px solid #263241; overflow: hidden; }
/* ===== titlebar ===== */
.tbar { height: 38px; background: #0a0e13; border-bottom: 1px solid #1d2633; display: flex;
align-items: center; justify-content: space-between; padding-left: 12px; flex-shrink: 0; }
.tbar-l { display: flex; align-items: center; gap: 10px; }
.tbar-logo { width: 18px; height: 18px; border-radius: 4px; background: linear-gradient(180deg,#25d386,#16a765);
display: flex; align-items: center; justify-content: center; }
.tbar-title { font-size: 12.5px; color: #d8dee9; font-weight: 500; }
.tbar-sub { font-size: 11px; color: var(--faint); }
.telemetry { display: flex; align-items: center; gap: 10px; font-size: 11px; color: #8b96a6;
background: #131922; border: 1px solid #222c3a; padding: 3px 12px; border-radius: 999px; }
.telemetry .tdot { width: 6px; height: 6px; border-radius: 50%; background: var(--em); }
.telemetry .sep { color: #334155; }
.telemetry b { color: #cbd5e1; font-weight: 600; }
#pingMs { color: var(--em); }
.winbtns { display: flex; height: 100%; }
.wbtn { width: 44px; height: 100%; display: flex; align-items: center; justify-content: center;
color: #94a3b8; background: none; border: none; cursor: pointer; transition: .12s; }
.wbtn:hover { background: rgba(255,255,255,.08); color: #fff; }
.wbtn.close:hover { background: #e81123; color: #fff; }
/* ===== layout ===== */
.body { flex: 1; display: flex; min-height: 0; }
/* sidebar */
.side { width: 236px; background: var(--sidebar-bg); border-right: 1px solid #1e2734;
display: flex; flex-direction: column; justify-content: space-between; padding: 14px; flex-shrink: 0; }
.brand { display: flex; align-items: center; gap: 12px; padding: 2px 2px 16px; border-bottom: 1px solid #1c2430; }
.brand-logo { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(180deg,#24cf83,#16ad69);
display: flex; align-items: center; justify-content: center; box-shadow: 0 3px 14px rgba(25,200,126,.32); flex-shrink: 0; }
.brand-name { font-size: 21px; font-weight: 700; color: var(--em); letter-spacing: -.3px; line-height: 1; }
.brand-ver { font-size: 12px; color: #6e7b8b; }
.brand-tag { font-size: 10.5px; color: #8b96a6; display: block; margin-top: 4px; }
.navsec { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;
color: #586577; padding: 14px 8px 6px; }
.nav-item { display: flex; align-items: center; justify-content: space-between; width: 100%;
padding: 9px 11px; border-radius: 8px; font-size: 13px; font-weight: 500; color: #94a3b8;
background: transparent; border: 1px solid transparent; cursor: pointer; transition: .15s;
text-align: left; margin-bottom: 3px; font-family: inherit; }
.nav-item:hover { color: #e2e8f0; background: rgba(255,255,255,.04); }
.nav-item.active { color: #fff; background: linear-gradient(90deg, rgba(25,200,126,.16), rgba(25,200,126,.03));
border-color: rgba(25,200,126,.32); }
.nav-item .nl { display: flex; align-items: center; gap: 10px; }
.nav-item svg { flex-shrink: 0; }
.nav-badge { font-size: 10px; padding: 2px 7px; border-radius: 6px; font-weight: 600; }
.nb-em { background: rgba(25,200,126,.15); color: var(--em); }
.nb-red { background: var(--crimson-bg); color: var(--crimson-fg); border: 1px solid var(--crimson-bd); }
.nb-grey { background: #1e293b; color: #94a3b8; }
.side-info { background: #131922; border: 1px solid #222c3a; border-radius: 9px; padding: 10px 11px; margin-bottom: 10px; }
.side-row { display: flex; align-items: center; justify-content: space-between; font-size: 11px; color: #8b96a6; }
.side-row + .side-row { margin-top: 5px; }
.acct-chip { display: flex; align-items: center; gap: 10px; padding: 2px 4px; }
.acct-ava { width: 28px; height: 28px; border-radius: 50%; background: rgba(25,200,126,.2);
border: 1px solid rgba(25,200,126,.4); display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; color: var(--em); flex-shrink: 0; }
.acct-mail { font-size: 11.5px; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.acct-sub { font-size: 10px; color: var(--em); }
/* main */
.main { flex: 1; padding: 16px 18px; display: flex; flex-direction: column; min-width: 0; overflow-y: auto;
background-image: linear-gradient(to right, rgba(255,255,255,.014) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255,255,255,.014) 1px, transparent 1px);
background-size: 28px 28px; }
.toprow { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.connectbar { display: flex; align-items: center; background: var(--input-bg); border: 1px solid #263242;
border-radius: 9px; padding: 4px 4px 4px 12px; gap: 8px; flex: 1; max-width: 560px; transition: .15s; }
.connectbar:focus-within { border-color: var(--em); }
.cb-lbl { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: var(--em); flex-shrink: 0; }
.cb-input { background: transparent; border: none; outline: none; color: #fff; flex: 1; min-width: 120px;
font-family: 'Cascadia Mono', Consolas, monospace; font-size: 13px; }
.cb-input::placeholder { color: #556274; font-family: 'Segoe UI', sans-serif; }
.cb-go { background: var(--em); color: #08110c; font-weight: 700; font-size: 12px; padding: 7px 13px;
border-radius: 7px; border: none; cursor: pointer; display: flex; align-items: center; gap: 5px; transition: .15s; flex-shrink: 0; }
.cb-go:hover { background: #22db8d; }
.portable-pill { background: #8c949e; color: #11161d; font-size: 12px; font-weight: 700; padding: 4px 12px;
border-radius: 999px; border: none; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
.portable-note { font-size: 12px; color: #6d7888; margin-left: 8px; }
/* cards */
.grid2 { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 14px; }
.col { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.card { background: linear-gradient(180deg, rgba(27,35,46,.95), rgba(22,29,38,.95));
border: 1px solid var(--card-border); border-radius: 10px; padding: 16px;
box-shadow: 0 6px 20px rgba(0,0,0,.22); transition: border-color .2s; }
.card:hover { border-color: #313f52; }
.card h2 { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.tag-sm { font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 5px; }
.tag-em { background: rgba(25,200,126,.1); color: var(--em); border: 1px solid rgba(25,200,126,.3); }
/* id card */
.id-big { font-family: 'Cascadia Mono', Consolas, monospace; font-size: 34px; font-weight: 800;
color: #fff; letter-spacing: .5px; line-height: 1.15; user-select: text; }
.btn-copy { background: #232b38; border: 1px solid #384558; color: var(--fg); border-radius: 9px;
padding: 9px 14px; font-size: 13px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px;
cursor: pointer; transition: .15s; }
.btn-copy:hover { background: #2c3646; border-color: #4b5b70; }
.btn-copy:active { transform: translateY(1px); }
.fmt-btn { font-size: 10.5px; color: var(--faint); background: #11161d; border: 1px solid var(--card-border);
border-radius: 5px; padding: 2px 7px; cursor: pointer; transition: .12s; }
.fmt-btn:hover { color: #cbd5e1; }
.status-pill { display: inline-flex; align-items: center; gap: 8px; padding: 4px 13px; border-radius: 999px;
font-size: 12.5px; font-weight: 600; transition: .2s; }
.status-pill .sdot { width: 8px; height: 8px; border-radius: 50%; }
.sp-stopped { background: var(--crimson-bg); border: 1px solid var(--crimson-bd); color: var(--crimson-fg); }
.sp-stopped .sdot { background: var(--crimson-fg); }
.sp-running { background: rgba(25,200,126,.12); border: 1px solid rgba(25,200,126,.45); color: var(--em-br); }
.sp-running .sdot { background: var(--em-br); animation: pulse 1.8s infinite; }
.sp-conn { background: #33301c; border: 1px solid #7c6535; color: #d9b96a; }
.sp-conn .sdot { background: #d9b96a; }
@keyframes pulse { 0%,100%{box-shadow:0 0 0 0 rgba(37,219,142,.6)} 50%{box-shadow:0 0 0 6px rgba(37,219,142,0)} }
.e2e-tag { display: inline-flex; align-items: center; gap: 5px; background: #11161d; border: 1px solid #242e3d;
padding: 3px 9px; border-radius: 6px; font-size: 11.5px; color: #94a3b8; }
/* inputs */
.nd-input { width: 100%; height: 38px; background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 7px; padding: 0 12px; font-size: 13.5px; color: #f0f6fc; outline: none; transition: .16s; }
.nd-input::placeholder { color: #556274; }
.nd-input:focus { border-color: var(--em); box-shadow: 0 0 0 3px rgba(25,200,126,.15); }
.field { margin-bottom: 12px; }
.field label { display: block; font-size: 13px; color: #9aa6b5; margin-bottom: 6px; }
.field .opt { color: #637081; }
.icon-btn { width: 38px; height: 38px; border-radius: 7px; background: #262f3d; border: 1px solid #384556;
color: #d0d8e2; display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
flex-shrink: 0; transition: .15s; }
.icon-btn:hover { background: #303b4c; border-color: #4b5b70; color: #fff; }
.icon-btn:active { transform: translateY(1px); }
.link-btn { background: none; border: none; color: var(--blue); font-size: 13.5px; font-weight: 600;
cursor: pointer; padding: 4px; flex-shrink: 0; }
.link-btn:hover { color: #66b3ff; text-decoration: underline; }
.badge-2fa { background: var(--crimson-bg); border: 1px solid var(--crimson-bd); color: var(--crimson-fg);
border-radius: 6px; padding: 8px 10px; font-size: 12.5px; font-weight: 600; cursor: pointer; flex-shrink: 0; transition: .15s; }
.badge-2fa.on { background: rgba(25,200,126,.12); border-color: rgba(25,200,126,.45); color: var(--em-br); }
.helper { font-size: 12.5px; color: #738091; line-height: 1.5; margin-top: 12px; }
.card-foot { margin-top: 14px; padding-top: 12px; border-top: 1px solid #222b38; display: flex;
align-items: center; justify-content: space-between; gap: 8px; font-size: 11.5px; color: #8b96a6; flex-wrap: wrap; }
.gr-link { color: var(--em); background: none; border: none; cursor: pointer; font-weight: 500; font-size: 11.5px; }
.gr-link:hover { text-decoration: underline; }
/* account */
.acct-line { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.acct-ok { font-size: 13.5px; color: var(--em); font-weight: 500; }
.fleet { margin-top: 14px; padding-top: 12px; border-top: 1px solid #222b38; }
.fleet-head { display: flex; align-items: center; justify-content: space-between; font-size: 11px;
color: #8b96a6; margin-bottom: 8px; }
.fleet-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.fleet-card { background: #11161d; border: 1px solid #253040; border-radius: 7px; padding: 7px 10px;
display: flex; align-items: center; justify-content: space-between; gap: 6px; min-width: 0; transition: .15s; }
.fleet-card.click { cursor: pointer; } .fleet-card.click:hover { border-color: rgba(25,200,126,.5); }
.fleet-name { font-size: 11.5px; font-weight: 600; color: #d0d8e2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fleet-sub { font-size: 10px; color: var(--faint); }
.fleet-sub.on { color: var(--em); }
.fdot { width: 8px; height: 8px; border-radius: 50%; background: #64748b; flex-shrink: 0; }
.fdot.on { background: var(--em); }
/* start/stop */
.startrow { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 14px; }
.btn-start { height: 44px; border-radius: 8px; background: linear-gradient(180deg,#22d084,#16b56f);
border: 1px solid #35e699; box-shadow: 0 0 22px rgba(25,200,126,.35), inset 0 1px 0 rgba(255,255,255,.22);
color: #fff; font-size: 14.5px; font-weight: 700; display: flex; align-items: center; justify-content: center;
gap: 9px; cursor: pointer; transition: .18s; font-family: inherit; }
.btn-start:hover { filter: brightness(1.06); }
.btn-start:active { transform: translateY(1px); }
.btn-stop { height: 44px; border-radius: 8px; background: linear-gradient(180deg,#555f6d,#48515e);
border: 1px solid #647080; color: #c7d0dc; font-size: 14px; font-weight: 600; display: flex;
align-items: center; justify-content: center; gap: 8px; cursor: pointer; transition: .18s; font-family: inherit; }
.btn-stop:hover { background: linear-gradient(180deg,#606b7b,#515b69); color: #fff; }
.stop-sq { width: 12px; height: 12px; border-radius: 2px; background: currentColor; }
/* toggles */
.togglecard { margin-top: 14px; padding: 14px 16px; }
.toggles { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; text-align: center; }
.tg { display: flex; flex-direction: column; align-items: center; }
.sw { width: 48px; height: 25px; border-radius: 999px; position: relative; cursor: pointer;
transition: .22s cubic-bezier(.4,0,.2,1); padding: 0; outline: none; margin-bottom: 8px; flex-shrink: 0; }
.sw.on { background: linear-gradient(180deg,#24d286,#18b872); border: 1px solid #39e59a;
box-shadow: 0 0 12px rgba(25,200,126,.38); }
.sw.off { background: #323b48; border: 1px solid #475364; box-shadow: inset 0 1px 3px rgba(0,0,0,.45); }
.sw-thumb { width: 19px; height: 19px; border-radius: 50%; position: absolute; top: 2px; left: 2px;
transition: transform .22s cubic-bezier(.4,0,.2,1), background .2s; }
.sw.on .sw-thumb { transform: translateX(23px); background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4); }
.sw.off .sw-thumb { background: #8b96a6; }
.tg-lb { font-size: 12.5px; color: #9aa6b5; line-height: 1.3; }
/* footer buttons */
.footrow { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 14px; }
.btn-foot { height: 40px; background: #161d26; border: 1px solid #1f7a53; border-radius: 8px;
box-shadow: 0 0 10px rgba(25,200,126,.12); color: #d9e2ec; font-size: 13.5px; font-weight: 500;
display: flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer; transition: .16s; font-family: inherit; }
.btn-foot:hover { background: #1c2632; border-color: #27be7c; color: #fff; }
.btn-foot svg { stroke: var(--em); }
/* views */
.view { display: none; } .view.active { display: block; }
.list-row { display: flex; align-items: center; justify-content: space-between; gap: 10px;
background: #11161d; border: 1px solid #253040; border-radius: 8px; padding: 11px 14px; margin-bottom: 8px; }
.logbox { background: #0a0e13; border: 1px solid #1e2734; border-radius: 8px; padding: 12px;
font-family: 'Cascadia Mono', Consolas, monospace; font-size: 11.5px; color: #9fb0c4; line-height: 1.7;
max-height: 480px; overflow-y: auto; white-space: pre-wrap; user-select: text; }
/* modal + toast */
.modal-bg { position: fixed; inset: 38px 0 0 0; background: rgba(7,10,15,.8); backdrop-filter: blur(5px);
z-index: 40; display: none; align-items: center; justify-content: center; padding: 20px; }
.modal-bg.open { display: flex; }
.modal { width: 100%; max-width: 500px; background: #161d27; border: 1px solid #2a3648; border-radius: 12px;
box-shadow: 0 24px 70px rgba(0,0,0,.7); overflow: hidden; }
.modal-h { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px;
background: #10151d; border-bottom: 1px solid #232d3c; }
.modal-h h3 { font-size: 14px; font-weight: 700; color: var(--em); }
.modal-x { background: none; border: none; color: #8b96a6; cursor: pointer; font-size: 14px; padding: 2px 8px; }
.modal-x:hover { color: #fff; }
.modal-b { padding: 16px; font-size: 13px; color: #cbd5e1; line-height: 1.6; max-height: 440px; overflow-y: auto; }
.modal-f { padding: 12px 16px; background: #10151d; border-top: 1px solid #232d3c; display: flex; justify-content: flex-end; gap: 8px; }
.modal-ok { padding: 8px 18px; border-radius: 7px; background: var(--em); color: #07120c; font-weight: 700;
font-size: 12.5px; border: none; cursor: pointer; }
.modal-ok:hover { background: #22db8d; }
.mono-box { font-family: Consolas, monospace; background: var(--input-bg); color: var(--em);
padding: 2px 8px; border-radius: 5px; user-select: text; word-break: break-all; }
#toasts { position: fixed; bottom: 16px; right: 16px; z-index: 50; display: flex; flex-direction: column; gap: 8px; pointer-events: none; }
.toast { background: rgba(19,25,34,.96); border: 1px solid #2c3849; border-left: 3px solid var(--em);
border-radius: 9px; padding: 10px 14px; box-shadow: 0 10px 30px rgba(0,0,0,.5); font-size: 12px;
transition: .25s; min-width: 220px; }
.toast b { color: #fff; display: block; }
.toast span { color: #94a3b8; font-size: 11px; }
::-webkit-scrollbar { width: 7px; height: 7px; }
::-webkit-scrollbar-track { background: #0d1117; }
::-webkit-scrollbar-thumb { background: #283342; border-radius: 3px; }
/* fitZoom keeps the effective layout ≥1080px, and CSS zoom does NOT affect
media queries - so collapse only at truly tiny raw window widths */
@media (max-width: 660px) { .grid2 { grid-template-columns: 1fr; } .toggles { grid-template-columns: repeat(3,1fr); row-gap: 16px; } .fleet-grid { grid-template-columns: 1fr 1fr; } }
/* ---- fleet toolbar / chips / allowlist (1.5) ---- */
.main { overflow-x: hidden; }
.dev-toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.dev-search { flex: 1; min-width: 200px; height: 34px; background: #0d131b; border: 1px solid #263241;
border-radius: 8px; color: #e2e8f0; font-size: 12.5px; padding: 0 12px; outline: none; }
.dev-search:focus { border-color: #2f6f4f; }
.chips { display: inline-flex; gap: 6px; }
.chip { height: 28px; padding: 0 12px; border-radius: 999px; border: 1px solid #263241;
background: #0d131b; color: #94a3b8; font-size: 11.5px; cursor: pointer; }
.chip.active { background: rgba(34,197,94,.12); border-color: #2f6f4f; color: #4ade80; }
.wake-btn { height: 28px; padding: 0 12px; border-radius: 7px; border: 1px solid #3b4a5e;
background: #17202b; color: #cbd5e1; font-size: 11.5px; cursor: pointer; }
.wake-btn:hover { border-color: #4ade80; color: #4ade80; }
#qrWrap { display: flex; }
</style>
</head>
<body>
<div class="shell">
<!-- titlebar -->
<header class="tbar">
<div class="tbar-l pywebview-drag-region" style="flex:1;height:100%;align-self:stretch">
<span class="tbar-logo"><svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="M12 3.2L3.2 12.6H20.8L12 3.2Z" fill="#fff"/><path d="M8.7 15.1H15.3L16.4 20.5H7.6L8.7 15.1Z" fill="#fff"/></svg></span>
<span class="tbar-title">NetDesk</span>
<span class="tbar-sub">— Enterprise Remote Desktop &amp; Unattended Access</span>
</div>
<div class="telemetry pywebview-drag-region">
<span style="display:flex;align-items:center;gap:6px"><span class="tdot" id="relayDot"></span>Relay: <b class="mono" id="relayHost">…</b></span>
<span class="sep">|</span><span class="mono" id="pingMs">— ms</span>
<span class="sep">|</span><span>TLS 1.3 • AES-256-GCM</span>
</div>
<div class="winbtns">
<button class="wbtn" id="winMin" title="Minimize"><svg width="11" height="11" viewBox="0 0 11 11"><rect x="1" y="5" width="9" height="1" fill="currentColor"/></svg></button>
<button class="wbtn" id="winMax" title="Maximize"><svg width="10" height="10" viewBox="0 0 10 10" fill="none"><rect x="1" y="1" width="8" height="8" stroke="currentColor"/></svg></button>
<button class="wbtn close" id="winClose" title="Close"><svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M1.5 1.5L9.5 9.5M9.5 1.5L1.5 9.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></button>
</div>
</header>
<div class="body">
<!-- sidebar -->
<aside class="side">
<div>
<div class="brand">
<div class="brand-logo"><svg width="23" height="23" viewBox="0 0 24 24" fill="none"><path d="M12 3.2L3.2 12.6C2.8 13 3.1 13.7 3.7 13.7H20.3C20.9 13.7 21.2 13 20.8 12.6L12 3.2Z" fill="#fff"/><path d="M8.7 15.1H15.3L16.4 20.3C16.5 20.7 16.2 21 15.8 21H8.2C7.8 21 7.5 20.7 7.6 20.3L8.7 15.1Z" fill="#fff"/></svg></div>
<div><span class="brand-name">NetDesk</span> <span class="brand-ver">v1.5.0</span>
<span class="brand-tag">Zero-Trust Remote Agent</span></div>
</div>
<div class="navsec">Workspace</div>
<button class="nav-item active" data-view="dash">
<span class="nl"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#19c87e" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg><span>Remote Control</span></span>
<span class="nav-badge nb-em">Host</span></button>
<button class="nav-item" data-view="devs">
<span class="nl"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#60a5fa" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/></svg><span>My Computers</span></span>
<span class="nav-badge nb-grey mono" id="navDevCount">0</span></button>
<button class="nav-item" data-view="sec">
<span class="nl"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg><span>Security &amp; 2FA</span></span>
<span class="nav-badge nb-red" id="nav2fa">2FA Off</span></button>
<button class="nav-item" data-view="logs">
<span class="nl"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#a78bfa" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg><span>Session Logs</span></span>
<span class="nav-badge nb-grey mono" id="navLogN">·</span></button>
</div>
<div>
<div class="side-info">
<div class="side-row"><span>Local Endpoint</span><span class="mono" style="color:var(--em)" id="sideHost">…</span></div>
<div class="side-row"><span>Daemon Status</span><span id="sideStatus" style="color:var(--crimson-fg);font-weight:600">● Stopped</span></div>
</div>
<div class="acct-chip" id="sideAcct" style="display:none">
<div class="acct-ava" id="sideAva">–</div>
<div style="min-width:0"><div class="acct-mail" id="sideMail"></div><div class="acct-sub">✓ Cloud Synced</div></div>
</div>
</div>
</aside>
<!-- main -->
<main class="main">
<div class="toprow">
<div class="connectbar">
<span class="cb-lbl">Connect to Partner:</span>
<input class="cb-input" id="partnerId" placeholder="Enter remote Partner ID (e.g. 842 190 441)" maxlength="12">
<button class="cb-go" id="partnerGo">Connect
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></button>
</div>
<div style="display:flex;align-items:center;flex-shrink:0">
<button class="portable-pill" id="portableBtn"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg> Portable</button>
<span class="portable-note">settings next to the app</span>
</div>
</div>
<!-- DASHBOARD VIEW -->
<div class="view active" id="view-dash">
<div class="grid2">
<div class="col">
<!-- ID card -->
<section class="card">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px">
<div>
<div style="display:flex;align-items:center;gap:8px"><h2>This Computer's ID</h2>
<button class="fmt-btn" id="fmtBtn" title="Toggle spaced format">Format</button></div>
<div class="id-big" id="bigId">…</div>
</div>
<button class="btn-copy" id="copyBtn"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><span id="copyLbl">Copy ID</span></button>
</div>
<div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
<span class="status-pill sp-stopped" id="statusPill"><span class="sdot"></span><span id="statusTxt">Stopped</span></span>
<span class="e2e-tag"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#19c87e" stroke-width="2.2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> E2EE Ready</span>
</div>
</section>
<!-- Password card -->
<section class="card" style="flex:1">
<div class="card-head"><h2>Unattended Password</h2>
<span class="tag-sm tag-em" id="pwBadge">PBKDF2 · 200k</span></div>
<div style="display:flex;align-items:center;gap:8px">
<input type="password" id="pwInput" class="nd-input mono" style="letter-spacing:1px" placeholder="no password yet — generate one">
<button class="icon-btn" id="pwEye" title="Show / hide"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="icon-btn" id="pwGen" title="Generate a strong random password"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></button>
<button class="link-btn" id="pwSet">Set</button>
<button class="badge-2fa" id="twoFa">2FA: off</button>
</div>
<p class="helper">Anyone who knows this ID and password can control this PC. Set a strong
password; turn on 2FA for an extra authenticator code at connect.</p>
<div class="card-foot">
<span style="display:flex;align-items:center;gap:6px"><span style="width:6px;height:6px;border-radius:50%;background:var(--em)"></span>Brute-force throttle: <b style="color:#cbd5e1">0.4s / attempt</b></span>
<button class="gr-link" id="go2fa">Configure Authenticator App →</button>
</div>
</section>
</div>
<div class="col">
<!-- Connection card -->
<section class="card">
<div class="card-head"><h2>Connection</h2>
<button class="gr-link" id="pingBtn" style="background:rgba(25,200,126,.1);border:1px solid rgba(25,200,126,.3);border-radius:5px;padding:3px 9px">Ping Relay</button></div>
<div class="field"><label for="srvIn">Server Address</label>
<input id="srvIn" class="nd-input mono" style="font-size:13px" spellcheck="false"></div>
<div class="field"><label for="regIn">Registration Key <span class="opt">(optional)</span></label>
<input id="regIn" class="nd-input mono" style="font-size:13px" placeholder="Leave blank for open relay" spellcheck="false"></div>
<div class="field" style="margin-bottom:0"><label for="nameIn">Computer Name</label>
<input id="nameIn" class="nd-input" spellcheck="false"></div>
</section>
<!-- Account card -->
<section class="card" style="flex:1">
<div class="card-head"><h2>NetDesk Account</h2>
<span class="tag-sm tag-em" id="acctBadge" style="display:none">Fleet Synced</span></div>
<div id="acctOut">
<div class="field"><input id="accEmail" class="nd-input" type="email" placeholder="email" autocomplete="username"></div>
<div class="field"><input id="accPass" class="nd-input" type="password" placeholder="password" autocomplete="current-password"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<button class="btn-start" style="height:38px;font-size:13px" id="accIn">Sign In</button>
<button class="btn-stop" style="height:38px;font-size:13px" id="accUp">Create Account</button>
</div>
</div>
<div id="acctIn" style="display:none">
<div class="acct-line">
<span class="acct-ok">✓ Signed in as <b id="acctMail"></b></span>
<button class="link-btn" id="accOut2">Sign out</button>
</div>
<p class="helper" style="margin-top:8px">This PC shows under “My computers” on the website.</p>
<div class="fleet">
<div class="fleet-head"><span>LINKED DEVICES IN “MY COMPUTERS”</span>
<button class="link-btn" style="font-size:11px" id="fleetAll">View all</button></div>
<div class="fleet-grid" id="fleetGrid"></div>
</div>
</div>
</section>
</div>
</div>
<div class="startrow">
<button class="btn-start" id="startBtn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 3L3 12.6H21L12 3Z" fill="#fff"/><path d="M8.6 14.4H15.4L16.5 20.5H7.5L8.6 14.4Z" fill="#fff"/></svg><span id="startTxt">Start Service</span></button>
<button class="btn-stop" id="stopBtn"><span class="stop-sq"></span><span>Stop</span></button>
</div>
<section class="card togglecard">
<div class="toggles">
<div class="tg"><button class="sw off" id="swSvc" role="switch"><span class="sw-thumb"></span></button><span class="tg-lb">Service<br>Installed</span></div>
<div class="tg"><button class="sw off" id="swWin" role="switch"><span class="sw-thumb"></span></button><span class="tg-lb">Start with<br>Windows</span></div>
<div class="tg"><button class="sw off" id="swPriv" role="switch"><span class="sw-thumb"></span></button><span class="tg-lb">Privacy<br>Mode</span></div>
<div class="tg"><button class="sw off" id="swLock" role="switch"><span class="sw-thumb"></span></button><span class="tg-lb">Lock on<br>disconnect</span></div>
<div class="tg"><button class="sw on" id="swTray" role="switch"><span class="sw-thumb"></span></button><span class="tg-lb">Close to<br>tray</span></div>
</div>
</section>
<div class="footrow">
<button class="btn-foot" id="webBtn"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>Open Web Viewer</button>
<button class="btn-foot" id="logsBtn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>Logs Folder</button>
</div>
</div>
<!-- MY COMPUTERS VIEW -->
<div class="view" id="view-devs">
<section class="card">
<div class="card-head"><h2>My Computers</h2>
<span style="display:flex;gap:10px;align-items:center">
<button class="gr-link" id="devRefresh">↻ Refresh</button>
<button class="cb-go" id="enrollBtn" style="height:30px">+ Enroll Computer</button>
</span></div>
<div class="dev-toolbar">
<input class="dev-search" id="devSearch" type="text" placeholder="Search by name or ID…" spellcheck="false">
<span class="chips">
<button class="chip active" data-f="all">All</button>
<button class="chip" data-f="online">Online</button>
<button class="chip" data-f="offline">Offline</button>
</span>
</div>
<div id="devList"><p class="helper">Sign in to your NetDesk account to see the computers linked to it.</p></div>
</section>
</div>
<!-- SECURITY VIEW -->
<div class="view" id="view-sec">
<section class="card" style="margin-bottom:14px">
<div class="card-head"><h2>Two-Factor Authentication (TOTP)</h2><span class="badge-2fa" id="secTfaState">2FA: off</span></div>
<p class="helper" style="margin-top:0">Adds a 6-digit authenticator code (Google Authenticator, Authy, 1Password…)
required at every connect — even with the correct password.</p>
<div style="margin-top:12px;display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap">
<button class="btn-start" style="height:38px;font-size:13px;max-width:280px" id="secTfaBtn">Enable 2FA</button>
<div id="qrWrap" style="display:none;align-items:center;gap:14px">
<img id="qrImg" alt="TOTP QR" width="132" height="132"
style="border-radius:10px;background:#fff;padding:4px">
<div style="min-width:0">
<div class="lbl" style="margin-bottom:4px">Scan with your authenticator app</div>
<div class="mono" id="qrSecret" style="font-size:11px;color:var(--faint);word-break:break-all;max-width:320px"></div>
</div>
</div>
</div>
</section>
<section class="card" style="margin-bottom:14px">
<div class="card-head"><h2>Viewer IP Allowlist</h2><span class="tag-sm tag-em" id="allowState">off — everyone with the password</span></div>
<p class="helper" style="margin-top:0">Only these IPs / CIDR ranges may connect (e.g. <span class="mono">203.0.113.7, 10.0.0.0/8</span>).
Leave empty to allow any IP that knows the password. Applies to the next connection.</p>
<div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap">
<input class="dev-search mono" id="allowIn" style="flex:1;min-width:260px" type="text"
placeholder="empty — all IPs allowed" spellcheck="false">
<button class="cb-go" id="allowSave" style="height:38px">Save Allowlist</button>
</div>
<p class="helper" style="margin-top:10px"><button class="gr-link" id="reqPreview">Preview the incoming-connection dialog →</button></p>
</section>
<section class="card">
<div class="card-head"><h2>Boot-Time Service</h2><span class="tag-sm tag-em" id="secSvcState">not installed</span></div>
<p class="helper" style="margin-top:0">Installs a SYSTEM service so this PC is reachable at the logon screen,
across reboots and UAC prompts — no one needs to be logged in. Requires one UAC approval.</p>
<div style="margin-top:12px;display:flex;gap:10px;flex-wrap:wrap">
<button class="btn-start" style="height:38px;font-size:13px;min-width:200px" id="secSvcBtn">Install Service</button>
<button class="btn-stop" style="height:38px;font-size:13px;min-width:160px" id="secSvcRm">Remove Service</button>
</div>
</section>
</div>
<!-- LOGS VIEW -->
<div class="view" id="view-logs">
<section class="card">
<div class="card-head"><h2>Session Audit Log</h2>
<span style="display:flex;gap:12px;align-items:center">
<span class="chips">
<button class="chip lchip active" data-lf="all">All</button>
<button class="chip lchip" data-lf="session">Sessions</button>
<button class="chip lchip" data-lf="error">Errors</button>
</span>
<button class="gr-link" id="logRefresh">↻ Refresh</button>
<button class="gr-link" id="logExport">⇩ Export</button>
<button class="gr-link" id="logOpen">Open folder</button></span></div>
<div class="logbox" id="logBox">Loading…</div>
</section>
</div>
</main>
</div>
<!-- modal -->
<div class="modal-bg" id="modalBg">
<div class="modal">
<div class="modal-h"><h3 id="mTitle">NetDesk</h3><button class="modal-x" id="mX">✕</button></div>
<div class="modal-b" id="mBody"></div>
<div class="modal-f"><button class="modal-ok" id="mOk">Close</button></div>
</div>
</div>
<div id="toasts"></div>
</div>
<script>
(function () {
"use strict";
window.__errs = [];
window.onerror = function (m, s, l, c) { window.__errs.push(m + " @" + l + ":" + c);
try { window.pywebview.api.log_err(m + " @" + l + ":" + c); } catch (e) {} };
function api() { return window.pywebview && window.pywebview.api; }
function call(m) {
var args = [].slice.call(arguments, 1);
var run = function () { var a = api(); if (!a) throw new Error("bridge"); return a[m].apply(a, args); };
return Promise.resolve().then(run).catch(function (e) {
return new Promise(function (r) { setTimeout(r, 700); }).then(function () { return run(); });
}).catch(function (e) { window.__errs.push(m + ": " + e); throw e; });
}
var $ = function (id) { return document.getElementById(id); };
/* ---------- modal + toasts ---------- */
function openModal(title, html, okLabel, onOk) {
$("mTitle").textContent = title; $("mBody").innerHTML = html;
$("mOk").textContent = okLabel || "Close";
$("mOk").onclick = function () { if (onOk && onOk() === false) return; closeModal(); };
$("modalBg").classList.add("open");
}
function closeModal() { $("modalBg").classList.remove("open"); }
$("mX").onclick = closeModal;
$("modalBg").addEventListener("click", function (e) { if (e.target === this) closeModal(); });
function toast(title, sub) {
var t = document.createElement("div"); t.className = "toast";
var b = document.createElement("b"); b.textContent = title;
var s = document.createElement("span"); s.textContent = sub || "";
t.append(b, s); $("toasts").append(t);
setTimeout(function () { t.style.opacity = "0"; t.style.transform = "translateY(6px)";
setTimeout(function () { t.remove(); }, 250); }, 2600);
}
/* ---------- auto-fit: the layout is designed at ~1080px; zoom the page so
it always fits the window with NO horizontal scroll, at any size ------- */
function fitZoom() {
var z = Math.min(1, window.innerWidth / 1080, window.innerHeight / 850);
z = Math.max(0.72, z);
document.documentElement.style.zoom = z;
// vh units are NOT scaled by CSS zoom - size the shell to the real window
var sh = document.querySelector(".shell");
if (sh) sh.style.height = Math.round(window.innerHeight / z) + "px";
window.scrollTo(0, 0);
}
window.addEventListener("resize", fitZoom); fitZoom();
/* ---------- window controls ---------- */
$("winMin").onclick = function () { call("minimize"); };
$("winMax").onclick = function () { call("maximize_window"); };
$("winClose").onclick = function () { call("close_request"); };
/* ---------- sidebar views ---------- */
var navBtns = document.querySelectorAll(".nav-item");
navBtns.forEach(function (b) {
b.onclick = function () {
navBtns.forEach(function (x) { x.classList.remove("active"); });
b.classList.add("active");
document.querySelectorAll(".view").forEach(function (v) { v.classList.remove("active"); });
$("view-" + b.dataset.view).classList.add("active");
if (b.dataset.view === "logs") loadLogs();
if (b.dataset.view === "devs") loadDevs();
};
});
/* ---------- state paint ---------- */
var S = { id: "", spaced: true, busySvc: false, acct: "" };
function fmtId(id) { return S.spaced ? String(id).replace(/(\d{3})(\d{3})(\d{3})/, "$1 $2 $3") : String(id); }
function setSwitch(id, on) { var b = $(id); b.classList.toggle("on", !!on); b.classList.toggle("off", !on);
b.setAttribute("aria-checked", String(!!on)); }
function paint(s) {
S.id = s.id || "";
$("bigId").textContent = fmtId(S.id);
if (document.activeElement !== $("srvIn")) $("srvIn").value = s.server || "";
if (document.activeElement !== $("regIn")) $("regIn").value = s.regKey || "";
if (document.activeElement !== $("nameIn")) $("nameIn").value = s.name || "";
if (document.activeElement !== $("pwInput") && s.password) $("pwInput").value = s.password;
try { $("relayHost").textContent = new URL((s.server || "").replace("wss","https").replace("ws","http")).host || "—"; }
catch (e) { $("relayHost").textContent = "—"; }
$("sideHost").textContent = s.name || "—";
var st = s.state === "online" && s.inSession ? "session" : s.state;
var pill = $("statusPill"), txt = $("statusTxt"), sideS = $("sideStatus");
if (st === "online" || st === "running" || st === "session") {
pill.className = "status-pill sp-running"; txt.textContent = st === "session" ? "In Session" : "Online";
sideS.textContent = "● Online"; sideS.style.color = "var(--em-br)";
$("startTxt").textContent = "Service Running";
} else if (st === "connecting") {
pill.className = "status-pill sp-conn"; txt.textContent = "Connecting…";
sideS.textContent = "● Connecting"; sideS.style.color = "#d9b96a";
$("startTxt").textContent = "Start Service";
} else {
pill.className = "status-pill sp-stopped"; txt.textContent = "Stopped";
sideS.textContent = "● Stopped"; sideS.style.color = "var(--crimson-fg)";
$("startTxt").textContent = "Start Service";
}
setSwitch("swSvc", s.serviceInstalled); setSwitch("swWin", s.startup);
setSwitch("swPriv", s.privacy); setSwitch("swLock", s.lockOnEnd);
setSwitch("swTray", s.closeToTray !== false);
var tfa = s.totp ? "2FA: ON" : "2FA: off";
$("twoFa").textContent = tfa; $("twoFa").classList.toggle("on", !!s.totp);
$("secTfaState").textContent = tfa; $("secTfaState").classList.toggle("on", !!s.totp);
$("secTfaBtn").textContent = s.totp ? "Disable 2FA" : "Enable 2FA";
$("nav2fa").textContent = s.totp ? "2FA On" : "2FA Off";
$("nav2fa").className = "nav-badge " + (s.totp ? "nb-em" : "nb-red");
$("secSvcState").textContent = s.serviceInstalled ? "installed" : "not installed";
// security view: TOTP QR + allowlist
if (s.totp && !S.qrLoaded) {
S.qrLoaded = true; // in-flight guard; cleared on failure so it retries
call("totp_qr").then(function (r) {
if (r.ok) { $("qrImg").src = r.png; $("qrSecret").textContent = r.secret;
$("qrWrap").style.display = "flex"; }
else S.qrLoaded = false;
}).catch(function () { S.qrLoaded = false; });
} else if (!s.totp) { S.qrLoaded = false; $("qrWrap").style.display = "none"; }
if (document.activeElement !== $("allowIn") && s.allowIps !== undefined)
$("allowIn").value = s.allowIps;
var hasAllow = !!(s.allowIps || "").trim();
$("allowState").textContent = hasAllow ? "on — restricted" : "off — everyone with the password";
// account
S.acct = s.account || "";
$("acctOut").style.display = S.acct ? "none" : "block";
$("acctIn").style.display = S.acct ? "block" : "none";
$("acctBadge").style.display = S.acct ? "inline-block" : "none";
if (S.acct) {
$("acctMail").textContent = S.acct;
$("sideAcct").style.display = "flex"; $("sideMail").textContent = S.acct;
$("sideAva").textContent = S.acct.slice(0, 2).toUpperCase();
} else { $("sideAcct").style.display = "none"; }
}
function refresh() { return call("get_state").then(paint).catch(function () {}); }
function poll() { refresh(); setTimeout(poll, 1500); }
/* ---------- ID card ---------- */
$("fmtBtn").onclick = function () { S.spaced = !S.spaced; $("bigId").textContent = fmtId(S.id); };
$("copyBtn").onclick = function () {
call("copy_id").then(function () { $("copyLbl").textContent = "Copied!"; toast("ID copied", S.id);
setTimeout(function () { $("copyLbl").textContent = "Copy ID"; }, 1400); });
};
/* ---------- password ---------- */
$("pwEye").onclick = function () { var i = $("pwInput"); i.type = i.type === "password" ? "text" : "password"; };
$("pwGen").onclick = function () {
openModal("Generate new password",
"Generate a strong random password?<br>The current one stops working for new connections.",
"Generate", function () {
call("generate_password").then(function (r) {
if (r.ok) { $("pwInput").type = "text"; $("pwInput").value = r.password;
openModal("New password", 'Your new unattended password:<br><span class="mono-box" style="font-size:17px;letter-spacing:1px">' +
r.password + "</span><br><br>It applies to the next incoming connection — no restart needed.");
} else openModal("Generate", r.error || "Failed.");
}); return false; });
};
$("pwSet").onclick = function () {
var v = $("pwInput").value;
if (!v) { $("pwInput").focus(); return; }
call("set_password", v).then(function (r) {
if (r.ok) toast("Password saved", "Applies to the next connection");
else openModal("Password", r.error || "Failed.");
});
};
function toggle2fa() {
call("toggle_2fa").then(function (r) {
if (!r.ok) return;
refresh();
if (r.on) openModal("2FA enabled",
'Add this secret to your authenticator app:<br><span class="mono-box">' + r.secret +
"</span><br><br>Also saved to:<br><span class=\"mono-box\">" + r.path + "</span>");
else toast("2FA disabled", "");
});
}
$("twoFa").onclick = toggle2fa; $("go2fa").onclick = toggle2fa; $("secTfaBtn").onclick = toggle2fa;
/* ---------- connection ---------- */
S.pingFails = 0;
function doPing() {
call("ping_relay").then(function (r) {
if (r.ok) S.pingFails = 0; else S.pingFails++;
// only declare offline after 2 consecutive failures (slow-line friendly)
var off = !r.ok && S.pingFails >= 2;
if (r.ok) { $("pingMs").textContent = r.ms + " ms"; $("pingBtn").textContent = "Ping Relay (" + r.ms + " ms)"; }
else if (off) $("pingMs").textContent = "offline";
$("relayDot").style.background = off ? "var(--crimson-fg)" : "var(--em)";
}).catch(function () {});
}
$("pingBtn").onclick = function () { doPing(); toast("Pinging relay…", ""); };
/* ---------- partner connect ---------- */
function goPartner() {
var v = $("partnerId").value;
call("open_partner", v).then(function (r) {
if (r.ok) toast("Opening session…", r.app ? "Remote screen opening in a NetDesk window" : "Opening the viewer in your browser");
else openModal("Connect to Partner", r.error || "Failed.");
});
}
$("partnerGo").onclick = goPartner;
$("partnerId").addEventListener("keydown", function (e) { if (e.key === "Enter") goPartner(); });
/* ---------- account ---------- */
$("accIn").onclick = function () {
call("account_login", $("accEmail").value, $("accPass").value).then(function (r) {
if (r.ok) { $("accPass").value = ""; toast("Signed in", r.email); refresh(); loadDevs(); }
else openModal("Sign in", r.error || "Failed.");
});
};
$("accUp").onclick = function () {
call("account_signup", $("accEmail").value, $("accPass").value).then(function (r) {
if (r.ok) { $("accPass").value = ""; refresh(); loadDevs();
openModal("Account created", "You are signed in. This PC now appears under “My computers”."); }
else openModal("Create account", r.error || "Failed.");
});
};
$("accPass").addEventListener("keydown", function (e) { if (e.key === "Enter") $("accIn").click(); });
$("accOut2").onclick = function () { call("account_logout").then(function () { refresh(); }); };
$("fleetAll").onclick = function () { document.querySelector('[data-view="devs"]').click(); };
/* fleet cache + filters */
S.devs = []; S.devFilter = "all"; S.devQ = "";
function devMatch(d) {
if (S.devFilter === "online" && !d.online) return false;
if (S.devFilter === "offline" && d.online) return false;
var q = S.devQ.toLowerCase();
return !q || (d.name || "").toLowerCase().indexOf(q) >= 0 || String(d.id).indexOf(q) >= 0;
}
function devRow(d, compact) {
var el = document.createElement("div");
el.className = compact ? "fleet-card" + (d.online ? " click" : "") : "list-row";
var left = document.createElement("div"); left.style.minWidth = "0";
var n = document.createElement("div"); n.className = "fleet-name"; n.textContent = d.name || d.id;
var s = document.createElement("div"); s.className = "fleet-sub" + (d.online ? " on" : "");
s.textContent = d.online ? (d.busy ? "In session" : "Online") : "Offline";
left.append(n, s);
el.append(left);
if (compact) {
var dot = document.createElement("span"); dot.className = "fdot" + (d.online ? " on" : ""); el.append(dot);
if (d.online) el.onclick = function () { call("open_partner", d.id); };
} else {
var right = document.createElement("div"); right.style.display = "flex"; right.style.gap = "8px"; right.style.alignItems = "center";
var idm = document.createElement("span"); idm.className = "mono"; idm.style.color = "var(--faint)"; idm.style.fontSize = "12px";
idm.textContent = d.id; right.append(idm);
if (d.online) { var go = document.createElement("button"); go.className = "cb-go"; go.textContent = "Connect";
go.onclick = function () { call("open_partner", d.id); }; right.append(go); }
else if (d.mac) { var wk = document.createElement("button"); wk.className = "wake-btn"; wk.textContent = "⚡ Wake";
wk.title = "Wake-on-LAN via another online computer on your account";
wk.onclick = function () {
wk.disabled = true; wk.textContent = "Waking…";
call("wake_device", d.id).then(function (r) {
wk.disabled = false; wk.textContent = "⚡ Wake";
if (r.ok) toast(r.already_online ? "Already online" : "Wake packet sent",
r.already_online ? "" : "Relayed via " + (r.relayed || 1) + " computer(s) on your LAN");
else openModal("Wake-on-LAN", r.error || "Failed.");
});
}; right.append(wk); }
el.append(right);
}
return el;
}
function renderDevs() {
var full = $("devList"); full.innerHTML = "";
var grid = $("fleetGrid"); grid.innerHTML = "";
var list = S.devs.filter(devMatch);
if (!list.length) {
var q = document.createElement("p"); q.className = "helper";
q.textContent = S.devs.length ? "No computers match this filter." :
"No computers linked yet — this PC appears here once the engine registers.";
full.append(q);
}
list.forEach(function (d) { full.append(devRow(d, false)); });
S.devs.slice(0, 3).forEach(function (d) { grid.append(devRow(d, true)); });
}
function loadDevs() {
call("list_devices").then(function (r) {
$("navDevCount").textContent = String((r.devices || []).length);
if (!r.ok) {
var full = $("devList"); full.innerHTML = "";
var p = document.createElement("p"); p.className = "helper";
p.textContent = r.error === "not signed in" ?
"Sign in to your NetDesk account to see the computers linked to it." :
"Could not load devices: " + (r.error || "");
full.append(p); return;
}
S.devs = r.devices || [];
renderDevs();
}).catch(function () {});
}
$("devRefresh").onclick = loadDevs;
$("devSearch").addEventListener("input", function () { S.devQ = this.value.trim(); renderDevs(); });
document.querySelectorAll(".chip[data-f]").forEach(function (c) {
c.onclick = function () {
document.querySelectorAll(".chip[data-f]").forEach(function (x) { x.classList.remove("active"); });
c.classList.add("active"); S.devFilter = c.dataset.f; renderDevs();
};
});
$("enrollBtn").onclick = function () {
var base = "https://netdesk.space";
try { base = new URL($("srvIn").value.replace("wss", "https").replace("ws", "http")).origin; } catch (e) {}
openModal("Enroll a new computer",
"<b>1.</b> On the new PC, download the agent:<br><span class=\"mono-box\">" + base + "/download</span><br><br>" +
"<b>2.</b> Run <span class=\"mono\">NetDeskHost.exe</span> — it gets its own 9-digit ID automatically.<br><br>" +
"<b>3.</b> Sign in with the same account (<span class=\"mono\">" + (S.acct || "your account") + "</span>) " +
"in its NetDesk Account card — the computer then appears here in My Computers.<br><br>" +
"<b>4.</b> Optional: turn on <i>Service Installed</i> there for unattended access at boot.");
};
/* ---------- start / stop ---------- */
$("startBtn").onclick = function () {
call("start_engine", $("srvIn").value, $("regIn").value, $("nameIn").value).then(function (r) {
if (r.ok) toast("Engine starting…", "Connecting to the relay");
else openModal("Start", r.error || "Failed.");
});
};
$("stopBtn").onclick = function () { call("stop_engine").then(function () { toast("Engine stopped", ""); refresh(); }); };
/* ---------- toggles ---------- */
function installService() {
if (S.busySvc) return; S.busySvc = true;
var done = function () { S.busySvc = false; refresh(); };
if ($("swSvc").classList.contains("on")) {
call("remove_service").then(function (r) { toast("Service removed", ""); }).then(done, done);
return;
}
openModal("Installing service",
"Accept the UAC prompt to continue. The service makes this PC reachable at boot, " +
"at the logon screen and across UAC prompts.");
call("install_service").then(function (r) {
if (r.ok) openModal("Service installed", (r.msg || "Installed.") +
"<br><br>Your PC is now reachable even after a reboot — before anyone logs in.");
else openModal("Install service", r.error || "Install failed.");
}).then(done, done);
}
$("swSvc").onclick = installService; $("secSvcBtn").onclick = installService;
$("secSvcRm").onclick = function () { call("remove_service").then(function () { toast("Service removed", ""); refresh(); }); };
$("swWin").onclick = function () {
var on = $("swWin").classList.contains("on");
call(on ? "remove_startup" : "add_startup").then(function () { refresh(); });
};
$("swPriv").onclick = function () {
var on = !$("swPriv").classList.contains("on");
setSwitch("swPriv", on);
if (on) openModal("Privacy mode", "Privacy mode turns the PHYSICAL monitor off while someone is " +
"connected. Anyone at the PC sees a no-signal screen. Only enable this if that is what you want.");
call("set_switches", { privacy: on });
};
$("swLock").onclick = function () {
var on = !$("swLock").classList.contains("on");
setSwitch("swLock", on); call("set_switches", { lock: on });
};
$("swTray").onclick = function () {
var on = !$("swTray").classList.contains("on");
setSwitch("swTray", on); call("set_close_to_tray", on);
};
/* ---------- footer / misc ---------- */
$("webBtn").onclick = function () { call("open_viewer"); };
$("logsBtn").onclick = function () { call("open_logs"); };
$("logOpen").onclick = function () { call("open_logs"); };
$("portableBtn").onclick = function () {
call("get_state").then(function (s) {
openModal("Portable mode", 'Configuration is stored next to the app:<br><span class="mono-box">' +
(s.configPath || "") + "</span>");
});
};
/* ---------- security extras ---------- */
$("allowSave").onclick = function () {
call("set_allow_ips", $("allowIn").value).then(function (r) {
if (r.ok) { toast("Allowlist saved", r.count ? r.count + " entr" + (r.count > 1 ? "ies" : "y") :
"cleared — all IPs allowed"); refresh(); }
else openModal("IP allowlist", r.error || "Failed.");
});
};
$("allowIn").addEventListener("keydown", function (e) { if (e.key === "Enter") $("allowSave").click(); });
$("reqPreview").onclick = function () {
openModal("Incoming connection request",
'<div style="text-align:center;padding:6px 0">' +
'<div style="font-size:34px;margin-bottom:8px">🖥️</div>' +
'<b style="font-size:15px">203.0.113.7 wants to control this desktop</b><br>' +
'<span style="color:var(--faint);font-size:12px">Password verified · E2E key exchanged · ' +
new Date().toLocaleTimeString() + "</span><br><br>" +
'<span style="color:#94a3b8;font-size:12px">This is a preview of what a viewer sees while connecting. ' +
"Access is only granted after the unattended password (and 2FA, when on) is verified.</span></div>",
"Got it");
};
/* ---------- logs view ---------- */
S.logLines = []; S.logFilter = "all";
function logMatch(l) {
var s = l.toLowerCase();
if (S.logFilter === "session") return s.indexOf("session") >= 0 || s.indexOf("viewer") >= 0 || s.indexOf("connect") >= 0;
if (S.logFilter === "error") return s.indexOf("error") >= 0 || s.indexOf("warn") >= 0 || s.indexOf("denied") >= 0 || s.indexOf("fail") >= 0;
return true;
}
function renderLogs() {
var lines = S.logLines.filter(logMatch);
$("logBox").textContent = lines.join("\n") || "No matching log entries.";
$("logBox").scrollTop = $("logBox").scrollHeight;
}
function loadLogs() {
call("get_logs", 300).then(function (r) {
S.logLines = r.lines || [];
$("navLogN").textContent = String(S.logLines.length);
renderLogs();
}).catch(function () {});
}
$("logRefresh").onclick = loadLogs;
document.querySelectorAll(".lchip").forEach(function (c) {
c.onclick = function () {
document.querySelectorAll(".lchip").forEach(function (x) { x.classList.remove("active"); });
c.classList.add("active"); S.logFilter = c.dataset.lf; renderLogs();
};
});
$("logExport").onclick = function () {
call("export_logs").then(function (r) {
if (r.ok) openModal("Log exported", 'Saved to:<br><span class="mono-box">' + r.path + "</span>");
else openModal("Export", r.error || "Failed.");
});
};
/* ---------- boot ---------- */
var ready = new Promise(function (res) {
if (api()) return res();
window.addEventListener("pywebviewready", res);
setTimeout(res, 4000);
});
ready.then(function () {
refresh().then(function () { poll(); loadDevs(); doPing(); });
setInterval(doPing, 12000);
setInterval(function () { if (S.acct) loadDevs(); }, 20000);
});
})();
</script>
</body>
</html>