NetDesk 1.5.0 — source release
This commit is contained in:
commit
40318aafdd
227 files changed
+80798
No files matched your search
@@ -0,0 +1,971 @@
|
||||
<!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 & 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 & 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>
|
||||
Reference in new issue
Block a user