summaryrefslogtreecommitdiff
path: root/cmd/client/frontend/dist
diff options
context:
space:
mode:
authorSergeiEU <39683682+SergeiEU@users.noreply.github.com>2026-04-01 10:17:15 +0400
committerSergeiEU <39683682+SergeiEU@users.noreply.github.com>2026-04-01 10:17:15 +0400
commit1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 (patch)
treed8c85273ede547e03a5727bf185f5d07e87b4a08 /cmd/client/frontend/dist
downloadvpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.gz
vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.tar.bz2
vpnem-1bd203c5555046b7ee4fbfe2f822eb3d03571ad7.zip
Initial importHEADmain
Diffstat (limited to 'cmd/client/frontend/dist')
-rw-r--r--cmd/client/frontend/dist/index.html538
1 files changed, 538 insertions, 0 deletions
diff --git a/cmd/client/frontend/dist/index.html b/cmd/client/frontend/dist/index.html
new file mode 100644
index 0000000..6a77f85
--- /dev/null
+++ b/cmd/client/frontend/dist/index.html
@@ -0,0 +1,538 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>vpnem</title>
+ <style>
+:root {
+ --bg: #191919;
+ --surface: #222;
+ --surface2: #2a2a2a;
+ --border: #333;
+ --border-focus: #555;
+ --text: #c8c4bd;
+ --text-dim: #706c64;
+ --text-faint: #4a4740;
+ --accent: #c9885a;
+ --accent-hover: #dda070;
+ --on: #7aad6a;
+ --on-dim: #3a5032;
+ --off: #b05050;
+ --off-dim: #4a2828;
+ --warn: #c9a84a;
+ --mono: 'Consolas', 'SF Mono', 'Menlo', monospace;
+}
+
+* { margin: 0; padding: 0; box-sizing: border-box; }
+html, body { height: 100%; }
+body {
+ font: 14px/1.5 'Segoe UI', 'Inter', system-ui, sans-serif;
+ background: var(--bg); color: var(--text);
+ user-select: none; overflow: hidden;
+}
+::-webkit-scrollbar { width:6px; }
+::-webkit-scrollbar-track { background:transparent; }
+::-webkit-scrollbar-thumb { background:#383838; border-radius:3px; }
+::-webkit-scrollbar-thumb:hover { background:#484848; }
+
+/* Layout */
+.app { display: flex; flex-direction: column; height: 100vh; }
+
+/* Header */
+.header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 10px 14px; border-bottom: 1px solid var(--border);
+ background: var(--surface); -webkit-app-region: drag;
+}
+.header .brand { font: 700 16px var(--mono); color: var(--accent); letter-spacing: 1.5px; text-transform: uppercase; }
+.header .conn-info { flex: 1; text-align: center; font: 12px var(--mono); color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 10px; transition: color .3s; }
+.header .conn-info.on { color: var(--text-dim); }
+.header .status { display: flex; align-items: center; gap: 8px; font-size: 13px; }
+.header .status .indicator {
+ width: 8px; height: 8px; border-radius: 50%; background: var(--off);
+ transition: background .3s;
+}
+.header .status .indicator.on { background: var(--on); }
+.header .status .label { color: var(--text-dim); transition: color .3s; }
+.header .status .label.on { color: var(--on); }
+
+/* Toast notifications */
+.toast {
+ position: fixed; bottom: 40px; left: 50%; transform: translateX(-50%);
+ padding: 8px 18px; font: 13px var(--mono); border-radius: 3px;
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text);
+ opacity: 0; transition: opacity .3s; pointer-events: none; z-index: 100;
+}
+.toast.show { opacity: 1; }
+.toast.ok { border-color: var(--on); color: var(--on); }
+.toast.err { border-color: var(--off); color: var(--off); }
+
+/* Nav */
+nav {
+ display: flex; border-bottom: 1px solid var(--border); background: var(--surface);
+}
+nav button {
+ flex: 1; padding: 10px 0; font: 600 12px/1 inherit; text-transform: uppercase;
+ letter-spacing: 1px; color: var(--text-dim); background: none;
+ border: none; border-bottom: 2px solid transparent; cursor: pointer;
+ transition: color .15s, border-color .15s;
+}
+nav button:hover { color: var(--text); }
+nav button.active { color: var(--accent); border-bottom-color: var(--accent); }
+
+/* Content area */
+.content { flex: 1; overflow-y: auto; padding: 16px 18px; }
+.panel { display: none; }
+.panel.active { display: block; }
+
+/* Form elements */
+label.field { display: block; margin-bottom: 14px; }
+label.field .lbl { display: block; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); margin-bottom: 6px; }
+select, input[type=text] {
+ width: 100%; padding: 9px 12px; font: 14px inherit; color: var(--text);
+ background: var(--surface); border: 1px solid var(--border); border-radius: 3px;
+ outline: none; cursor: pointer; transition: border-color .15s;
+}
+select:focus, input[type=text]:focus { border-color: var(--border-focus); }
+input[type=text] { cursor: text; }
+
+/* Primary action */
+.btn-primary {
+ width: 100%; padding: 12px; margin-top: 6px; font: 700 14px inherit;
+ text-transform: uppercase; letter-spacing: 1.5px; border: 1px solid var(--on);
+ border-radius: 3px; background: var(--on-dim); color: var(--on); cursor: pointer;
+ transition: all .15s;
+}
+.btn-primary:active { transform: scale(.98); }
+.btn-primary:hover { background: var(--on); color: var(--bg); }
+.btn-primary.disconnect { border-color: var(--off); background: var(--off-dim); color: var(--off); }
+.btn-primary.disconnect:hover { background: var(--off); color: var(--bg); }
+.btn-primary:disabled { opacity: .5; cursor: default; }
+
+/* Secondary buttons */
+.btn { display: inline-block; padding: 5px 12px; font: 11px inherit; color: var(--text-dim); background: var(--surface2); border: 1px solid var(--border); border-radius: 3px; cursor: pointer; }
+.btn:hover { color: var(--text); border-color: var(--border-focus); }
+
+/* Exit IP line */
+.meta { margin-top: 10px; font: 13px var(--mono); color: var(--text-dim); text-align: center; }
+.meta em { font-style: normal; color: var(--accent); }
+
+/* Checkbox row */
+.check-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 13px; color: var(--text-dim); }
+.check-row input[type=checkbox] { accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer; }
+
+/* Toggle list rows */
+.row {
+ display: flex; align-items: center; gap: 8px;
+ padding: 6px 8px; border-bottom: 1px solid var(--border);
+ font-size: 12px;
+}
+.row:last-child { border-bottom: none; }
+.row .name { flex: 1; font-family: var(--mono); font-size: 13px; color: var(--text); }
+.row .desc { flex: 2; color: var(--text-dim); font-size: 12px; }
+.row .badge { font: 600 9px var(--mono); text-transform: uppercase; letter-spacing: 0.5px; padding: 2px 5px; border-radius: 2px; }
+.row .badge.on { background: var(--on-dim); color: var(--on); }
+.row .badge.always { background: #333; color: var(--text-dim); }
+
+/* Toggle switch */
+.sw { position: relative; width: 32px; height: 18px; flex-shrink: 0; }
+.sw input { display: none; }
+.sw span {
+ position: absolute; inset: 0; background: #3a3a3a; border-radius: 9px;
+ cursor: pointer; transition: .2s;
+}
+.sw span::after {
+ content: ''; position: absolute; width: 12px; height: 12px;
+ left: 3px; top: 3px; background: #666; border-radius: 50%; transition: .2s;
+}
+.sw input:checked + span { background: var(--on-dim); }
+.sw input:checked + span::after { transform: translateX(14px); background: var(--on); }
+
+/* Process items */
+.proc { display: flex; align-items: center; padding: 6px 8px; font: 13px var(--mono); border-bottom: 1px solid var(--border); transition: background .1s; }
+.proc:hover { background: var(--surface); }
+.proc .pname { flex: 1; }
+.proc.builtin .pname { color: var(--text-dim); }
+.proc .x { background: none; border: none; color: var(--off); cursor: pointer; font-size: 13px; padding: 0 4px; }
+.proc .x:hover { color: #e06060; }
+.add-row { display: flex; gap: 4px; margin-top: 6px; }
+.add-row input { flex: 1; font-size: 12px; padding: 5px 8px; }
+.add-row .btn { padding: 5px 14px; }
+
+/* Latency rows */
+.lat { display: flex; padding: 6px 8px; font: 13px var(--mono); border-bottom: 1px solid var(--border); transition: background .1s; }
+.lat:hover { background: var(--surface); }
+.lat .tag { flex: 1; }
+.lat .ms { min-width: 55px; text-align: right; }
+.lat .ms.g { color: var(--on); }
+.lat .ms.m { color: var(--warn); }
+.lat .ms.b { color: var(--off); }
+.lat .ms.d { color: var(--text-faint); }
+
+/* Log viewer */
+.logview {
+ background: #111; border: 1px solid var(--border); border-radius: 3px;
+ padding: 8px 10px; max-height: 220px; overflow-y: auto;
+ font: 11px/1.7 var(--mono); color: var(--text-dim); white-space: pre-wrap; word-break: break-all;
+}
+
+/* Section headers inside panels */
+.sh { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-faint); margin: 14px 0 8px; }
+.sh:first-child { margin-top: 0; }
+
+/* Footer */
+.footer {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 8px 16px; border-top: 1px solid var(--border);
+ background: var(--surface); font-size: 12px; color: var(--text-faint);
+}
+.footer .update { color: var(--accent); cursor: pointer; }
+ </style>
+</head>
+<body>
+<div class="app">
+ <div class="header">
+ <span class="brand">vpnem</span>
+ <span class="conn-info" id="connInfo"></span>
+ <div class="status">
+ <div class="indicator" id="ind"></div>
+ <span class="label" id="statusLabel">offline</span>
+ </div>
+ </div>
+ <div class="toast" id="toast"></div>
+
+ <nav>
+ <button class="active" onclick="tab('vpn')">vpn</button>
+ <button onclick="tab('rules')">rules</button>
+ <button onclick="tab('bypass')">bypass</button>
+ <button onclick="tab('diag')">diag</button>
+ </nav>
+
+ <div class="content">
+ <!-- VPN panel -->
+ <div id="p-vpn" class="panel active">
+ <label class="field">
+ <span class="lbl">Server</span>
+ <select id="selServer"></select>
+ </label>
+ <label class="field">
+ <span class="lbl">Routing mode</span>
+ <select id="selMode"></select>
+ </label>
+ <button class="btn-primary" id="btnConn" onclick="doConnect()">Connect</button>
+ <div class="meta" id="exitIp"></div>
+ <div class="check-row">
+ <input type="checkbox" id="chkAuto" onchange="window.go.main.App.SetAutoConnect(this.checked)">
+ <span>Auto-connect on launch</span>
+ </div>
+ </div>
+
+ <!-- Rules panel -->
+ <div id="p-rules" class="panel">
+ <div class="sh">Rule sets</div>
+ <div id="rsList"></div>
+ </div>
+
+ <!-- Bypass panel -->
+ <div id="p-bypass" class="panel">
+ <div class="sh">Built-in exclusions</div>
+ <div id="procDefault"></div>
+ <div class="sh">Custom exclusions</div>
+ <div id="procCustom"></div>
+ <div class="add-row">
+ <input type="text" id="inpProc" placeholder="process.exe" onkeydown="if(event.key==='Enter')addProc()">
+ <button class="btn" onclick="addProc()">Add</button>
+ </div>
+ </div>
+
+ <!-- Diag panel -->
+ <div id="p-diag" class="panel">
+ <div class="sh">Latency <button class="btn" onclick="measureLat()" style="margin-left:6px">Measure</button></div>
+ <div id="latList"></div>
+ <div class="sh" style="margin-top:14px">Log <button class="btn" onclick="loadLog()" style="margin-left:6px">Refresh</button></div>
+ <div class="logview" id="logBox">No logs</div>
+ </div>
+ </div>
+
+ <div class="footer">
+ <span id="syncInfo">--</span>
+ <span>
+ <span id="updInfo" style="display:none" class="update"></span>
+ <button id="updBtn" style="display:none" class="btn" onclick="doUpdate()">update</button>
+ <button class="btn" onclick="doSync()">sync</button>
+ </span>
+ </div>
+</div>
+
+<script>
+let on = false;
+let toastTimer = null;
+const $ = s => document.getElementById(s);
+const A = () => window.go.main.App;
+
+function toast(msg, type) {
+ const t = $('toast');
+ t.textContent = msg;
+ t.className = 'toast show' + (type ? ' ' + type : '');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(() => t.className = 'toast', 3000);
+}
+
+function tab(name) {
+ document.querySelectorAll('nav button').forEach((b,i) => b.classList.toggle('active', b.textContent.trim() === name));
+ document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
+ $('p-' + name).classList.add('active');
+ if (name === 'rules') loadRules();
+ if (name === 'bypass') loadProcs();
+ if (name === 'diag') loadLog();
+}
+
+async function init() {
+ for (let i = 0; i < 50; i++) {
+ if (window.go && window.go.main && window.go.main.App) break;
+ await new Promise(r => setTimeout(r, 100));
+ }
+ if (!A()) { $('syncInfo').textContent = 'runtime error'; return; }
+
+ try {
+ // Load modes (always available, static)
+ const modes = await A().GetModes();
+ const sel = $('selMode');
+ (modes||[]).forEach(m => { const o = document.createElement('option'); o.value = m; o.textContent = m; sel.appendChild(o); });
+
+ // Wait for sync to complete (may still be running in background)
+ let servers = await A().GetServers();
+ if (!servers || !servers.length) {
+ $('syncInfo').textContent = 'syncing...';
+ // Wait up to 10s for background sync
+ for (let i = 0; i < 20; i++) {
+ await new Promise(r => setTimeout(r, 500));
+ servers = await A().GetServers();
+ if (servers && servers.length) break;
+ }
+ if (!servers || !servers.length) {
+ // Force sync
+ await A().Sync();
+ servers = await A().GetServers();
+ }
+ }
+
+ const status = await A().GetStatus();
+
+ fillServers(servers || []);
+
+ // Set saved or random NL server
+ if (status.server) {
+ $('selServer').value = status.server;
+ } else {
+ try {
+ const rnd = await A().RandomNLServer();
+ if (rnd) $('selServer').value = rnd;
+ } catch(e) {}
+ }
+
+ // Set saved mode or default to last (Combo)
+ if (status.mode) {
+ $('selMode').value = status.mode;
+ } else if (modes && modes.length) {
+ $('selMode').value = modes[modes.length - 1];
+ }
+ if (status.connected) { setOn(true); setTimeout(getIP, 2000); }
+ $('chkAuto').checked = !!status.autoConnect;
+ $('syncInfo').textContent = (servers||[]).length + ' servers';
+ } catch(e) { $('syncInfo').textContent = String(e); }
+
+ setTimeout(checkUpd, 5000);
+
+ // Listen for backend sync events and refresh UI
+ if (window.runtime && window.runtime.EventsOn) {
+ window.runtime.EventsOn('connected', (serverTag) => {
+ $('selServer').value = serverTag;
+ setOn(true, serverTag);
+ });
+ window.runtime.EventsOn('synced', async () => {
+ try {
+ const servers = await A().GetServers();
+ const status = await A().GetStatus();
+ $('selServer').textContent = '';
+ fillServers(servers || []);
+ if (status.server) {
+ $('selServer').value = status.server;
+ } else {
+ const rnd = await A().RandomNLServer();
+ if (rnd) $('selServer').value = rnd;
+ }
+ if (status.mode) $('selMode').value = status.mode;
+ $('syncInfo').textContent = (servers||[]).length + ' servers';
+ } catch(e) {}
+ });
+ }
+}
+
+function fillServers(list) {
+ const sel = $('selServer');
+ const groups = {};
+ list.forEach(s => { (groups[s.region] = groups[s.region]||[]).push(s); });
+ for (const [r, ss] of Object.entries(groups)) {
+ const g = document.createElement('optgroup'); g.label = r;
+ ss.forEach(s => { const o = document.createElement('option'); o.value = s.tag; o.textContent = s.tag + ' \u00b7 ' + s.type; g.appendChild(o); });
+ sel.appendChild(g);
+ }
+}
+
+async function doConnect() {
+ $('btnConn').disabled = true;
+ $('btnConn').textContent = on ? 'Disconnecting...' : 'Connecting...';
+ try {
+ if (on) {
+ await A().Disconnect();
+ setOn(false);
+ } else {
+ const srv = $('selServer').value;
+ const mode = $('selMode').value;
+ if (!srv) { toast('Select a server', 'err'); $('btnConn').disabled = false; $('btnConn').textContent = 'Connect'; return; }
+ await A().Connect(srv, mode);
+ setOn(true);
+ setTimeout(getIP, 2500);
+ }
+ } catch(e) {
+ setOn(false);
+ toast(String(e).substring(0, 80), 'err');
+ }
+ $('btnConn').disabled = false;
+}
+
+function setOn(state, serverTag) {
+ const wasOn = on;
+ on = state;
+ $('ind').className = 'indicator' + (state ? ' on' : '');
+ $('statusLabel').textContent = state ? 'connected' : 'offline';
+ $('statusLabel').className = 'label' + (state ? ' on' : '');
+ const b = $('btnConn');
+ b.textContent = state ? 'Disconnect' : 'Connect';
+ b.className = 'btn-primary' + (state ? ' disconnect' : '');
+ if (state) {
+ const srv = serverTag || $('selServer').value;
+ const mode = $('selMode').value;
+ $('connInfo').textContent = srv + ' \u00b7 ' + mode;
+ $('connInfo').className = 'conn-info on';
+ if (!wasOn) toast('Connected: ' + srv, 'ok');
+ } else {
+ $('connInfo').textContent = '';
+ $('connInfo').className = 'conn-info';
+ $('exitIp').textContent = '';
+ if (wasOn) toast('Disconnected', 'err');
+ }
+}
+
+async function getIP() {
+ try {
+ const ip = await A().GetExitIP();
+ if (ip) { $('exitIp').textContent = ''; const t = document.createTextNode('exit '); $('exitIp').appendChild(t); const e = document.createElement('em'); e.textContent = ip.trim(); $('exitIp').appendChild(e); }
+ } catch(e) {}
+}
+
+async function doSync() {
+ $('syncInfo').textContent = 'syncing\u2026';
+ try { await A().Sync(); const s = await A().GetServers(); $('selServer').textContent = ''; fillServers(s||[]); $('syncInfo').textContent = (s||[]).length + ' servers'; }
+ catch(e) { $('syncInfo').textContent = 'error'; }
+}
+
+// Rules
+async function loadRules() {
+ const c = $('rsList'); c.textContent = '';
+ try {
+ const rs = await A().GetRuleSets();
+ (rs||[]).forEach(r => {
+ const row = document.createElement('div'); row.className = 'row';
+ const nm = document.createElement('span'); nm.className = 'name'; nm.textContent = r.tag; row.appendChild(nm);
+ const ds = document.createElement('span'); ds.className = 'desc'; ds.textContent = r.description; row.appendChild(ds);
+ if (r.optional) {
+ const sw = document.createElement('label'); sw.className = 'sw';
+ const inp = document.createElement('input'); inp.type = 'checkbox'; inp.checked = r.enabled;
+ inp.onchange = () => A().SetRuleSetEnabled(r.tag, inp.checked);
+ const sp = document.createElement('span');
+ sw.appendChild(inp); sw.appendChild(sp); row.appendChild(sw);
+ } else {
+ const b = document.createElement('span'); b.className = 'badge always'; b.textContent = 'active'; row.appendChild(b);
+ }
+ c.appendChild(row);
+ });
+ } catch(e) {}
+}
+
+// Bypass processes
+async function loadProcs() {
+ try {
+ const d = await A().GetBypassProcesses();
+ const df = $('procDefault'); df.textContent = '';
+ (d.default||[]).forEach(p => { const r = document.createElement('div'); r.className = 'proc builtin'; const n = document.createElement('span'); n.className = 'pname'; n.textContent = p; r.appendChild(n); df.appendChild(r); });
+ const cf = $('procCustom'); cf.textContent = '';
+ (d.custom||[]).forEach(p => {
+ const r = document.createElement('div'); r.className = 'proc';
+ const n = document.createElement('span'); n.className = 'pname'; n.textContent = p; r.appendChild(n);
+ const x = document.createElement('button'); x.className = 'x'; x.textContent = '\u00d7'; x.onclick = async()=>{ await A().RemoveBypassProcess(p); loadProcs(); }; r.appendChild(x);
+ cf.appendChild(r);
+ });
+ } catch(e) {}
+}
+
+async function addProc() {
+ const v = $('inpProc').value.trim(); if (!v) return;
+ await A().AddBypassProcess(v); $('inpProc').value = ''; loadProcs();
+}
+
+// Latency
+async function measureLat() {
+ const c = $('latList'); c.textContent = 'measuring\u2026';
+ try {
+ const res = await A().MeasureLatency(); c.textContent = '';
+ res.forEach(r => {
+ const row = document.createElement('div'); row.className = 'lat';
+ const t = document.createElement('span'); t.className = 'tag'; t.textContent = r.tag; row.appendChild(t);
+ const m = document.createElement('span'); m.className = 'ms';
+ if (r.latency_ms < 0) { m.textContent = '\u2014'; m.classList.add('d'); }
+ else if (r.latency_ms < 80) { m.textContent = r.latency_ms + 'ms'; m.classList.add('g'); }
+ else if (r.latency_ms < 200) { m.textContent = r.latency_ms + 'ms'; m.classList.add('m'); }
+ else { m.textContent = r.latency_ms + 'ms'; m.classList.add('b'); }
+ row.appendChild(m); c.appendChild(row);
+ });
+ } catch(e) { c.textContent = String(e); }
+}
+
+async function loadLog() {
+ try { const l = await A().GetLogs(); $('logBox').textContent = (l||[]).join('\n') || 'empty'; $('logBox').scrollTop = $('logBox').scrollHeight; } catch(e) {}
+}
+
+async function checkUpd() {
+ try {
+ const i = await A().CheckUpdate();
+ if (i && i.available) {
+ $('updInfo').style.display = 'inline';
+ $('updInfo').textContent = 'v' + i.version + ' available';
+ $('updBtn').style.display = 'inline-block';
+ }
+ } catch(e) {}
+}
+
+async function doUpdate() {
+ $('updBtn').disabled = true;
+ $('updBtn').textContent = 'updating...';
+ $('updInfo').textContent = 'downloading, app will restart';
+ try {
+ await A().DownloadUpdate();
+ // If we get here, restart failed — shouldn't normally happen
+ $('updInfo').textContent = 'restart manually';
+ } catch(e) {
+ $('updBtn').textContent = 'failed';
+ $('updInfo').textContent = String(e).substring(0, 40);
+ setTimeout(() => { $('updBtn').textContent = 'retry'; $('updBtn').disabled = false; }, 3000);
+ }
+}
+
+setInterval(async()=>{ try { const s = await A().GetStatus(); if (s.connected !== on) { setOn(s.connected); if (s.connected) getIP(); } } catch(e) {} }, 5000);
+init();
+</script>
+</body>
+</html>