Release 3.9.0+1: Remnawave API configs, Remnawave-style UI with flags, Windows build.

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-08-01 16:17:10 +03:00
co-authored by Cursor
parent 92cf707597
commit 015ded9bd5
24 changed files with 985 additions and 159 deletions
+265 -125
View File
@@ -10,125 +10,126 @@
var k = "navis.theme";
var t = localStorage.getItem(k);
if (t !== "light" && t !== "dark") {
t = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
t = "dark";
}
document.documentElement.setAttribute("data-theme", t);
} catch (_) {
document.documentElement.setAttribute("data-theme", "light");
document.documentElement.setAttribute("data-theme", "dark");
}
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700;800&family=Figtree:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
/* Remnawave-inspired: dark-first slate + cyan accent (not purple slop) */
:root {
color-scheme: light;
--ink: #0b1c18;
--ink: #0f172a;
--text: var(--ink);
--muted: #5a736b;
--line: rgba(11, 28, 24, 0.10);
--accent: #0d8a66;
--accent-deep: #086b4f;
--accent-soft: #d8f3e9;
--danger: #c0362c;
--ok: #0d8a66;
--ms-good: #0a7a3e;
--ms-mid: #b8860b;
--surface: rgba(255, 255, 255, 0.82);
--surface-2: rgba(255, 255, 255, 0.62);
--input-bg: rgba(255, 255, 255, 0.94);
--row-bg: rgba(255, 255, 255, 0.6);
--hero-bg: linear-gradient(160deg, rgba(255,255,255,.92), rgba(232,247,240,.88));
--hero-bg-on: linear-gradient(160deg, rgba(216,243,233,.95), rgba(255,255,255,.9));
--chip-bg: rgba(255, 255, 255, 0.75);
--server-list-bg: rgba(255, 255, 255, 0.4);
--server-row-bg: rgba(255, 255, 255, 0.72);
--muted: #64748b;
--line: rgba(15, 23, 42, 0.10);
--accent: #0891b2;
--accent-deep: #0e7490;
--accent-soft: #cffafe;
--danger: #dc2626;
--ok: #0d9488;
--ms-good: #059669;
--ms-mid: #d97706;
--surface: rgba(255, 255, 255, 0.88);
--surface-2: rgba(248, 250, 252, 0.92);
--input-bg: #ffffff;
--row-bg: rgba(255, 255, 255, 0.7);
--hero-bg: linear-gradient(155deg, #f8fafc 0%, #ecfeff 55%, #f1f5f9 100%);
--hero-bg-on: linear-gradient(155deg, #ecfeff 0%, #e0f2fe 50%, #f8fafc 100%);
--chip-bg: rgba(255, 255, 255, 0.85);
--server-list-bg: rgba(241, 245, 249, 0.65);
--server-row-bg: rgba(255, 255, 255, 0.92);
--server-row-hover: #fff;
--server-row-active: linear-gradient(135deg, rgba(216,243,233,.95), #fff);
--meta-bg: rgba(255, 255, 255, 0.35);
--meta-ok-bg: rgba(216, 243, 233, 0.65);
--meta-err-bg: rgba(255, 236, 234, 0.75);
--shop-bg: linear-gradient(135deg, rgba(13,138,102,.09), rgba(255,255,255,.55));
--shop-border: rgba(13, 138, 102, 0.16);
--modal-bg: #f5fbf8;
--modal-scrim: rgba(8, 28, 24, 0.42);
--update-bg: linear-gradient(135deg, rgba(13,138,102,.14), rgba(255,255,255,.75));
--update-border: rgba(13, 138, 102, 0.28);
--icon-btn-bg: linear-gradient(180deg, #d2efe4, #bfe6d6);
--icon-btn-border: rgba(8, 90, 68, 0.22);
--icon-btn-hover: linear-gradient(180deg, #c5e9db, #aedfcb);
--mini-bg: linear-gradient(180deg, #d2efe4, #bfe6d6);
--mini-accent-bg: linear-gradient(180deg, #2f6f5f, #25584b);
--mini-accent-fg: #f2fff9;
--switch-off: #c7d5cf;
--server-row-active: linear-gradient(135deg, #ecfeff, #fff);
--meta-bg: rgba(241, 245, 249, 0.8);
--meta-ok-bg: rgba(204, 251, 241, 0.75);
--meta-err-bg: rgba(254, 226, 226, 0.85);
--shop-bg: linear-gradient(135deg, rgba(8,145,178,.08), rgba(255,255,255,.7));
--shop-border: rgba(8, 145, 178, 0.18);
--modal-bg: #f8fafc;
--modal-scrim: rgba(15, 23, 42, 0.45);
--update-bg: linear-gradient(135deg, rgba(8,145,178,.12), rgba(255,255,255,.85));
--update-border: rgba(8, 145, 178, 0.28);
--icon-btn-bg: linear-gradient(180deg, #e2e8f0, #cbd5e1);
--icon-btn-border: rgba(15, 23, 42, 0.12);
--icon-btn-hover: linear-gradient(180deg, #f1f5f9, #e2e8f0);
--mini-bg: linear-gradient(180deg, #e2e8f0, #cbd5e1);
--mini-accent-bg: linear-gradient(180deg, #0891b2, #0e7490);
--mini-accent-fg: #f0fdfa;
--switch-off: #cbd5e1;
--switch-knob: #fff;
--dot-off: #9aaba4;
--shadow: 0 18px 50px rgba(8, 40, 32, 0.12);
--radius: 22px;
--dot-off: #94a3b8;
--shadow: 0 20px 50px rgba(15, 23, 42, 0.10);
--radius: 18px;
--ease: cubic-bezier(.22,.8,.24,1);
--page-bg:
radial-gradient(780px 420px at 8% -10%, #9fe0c8 0%, transparent 55%),
radial-gradient(640px 380px at 100% 0%, #b8d4e8 0%, transparent 48%),
radial-gradient(500px 320px at 50% 110%, #cfe8dc 0%, transparent 45%),
linear-gradient(165deg, #e7f6f0 0%, #f4faf7 48%, #eef4f8 100%);
--theme-toggle-bg: rgba(255, 255, 255, 0.7);
radial-gradient(700px 380px at 0% -5%, #a5f3fc 0%, transparent 50%),
radial-gradient(560px 320px at 100% 0%, #bae6fd 0%, transparent 45%),
linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #eef2ff 100%);
--theme-toggle-bg: rgba(255, 255, 255, 0.75);
--sidebar-rail: rgba(8, 145, 178, 0.55);
}
html[data-theme="dark"] {
color-scheme: dark;
--ink: #e4f2eb;
--ink: #e8eef7;
--text: var(--ink);
--muted: #8fa89c;
--line: rgba(180, 220, 200, 0.12);
--accent: #2ec995;
--accent-deep: #1aa877;
--accent-soft: rgba(46, 201, 149, 0.16);
--danger: #e07068;
--ok: #2ec995;
--ms-good: #3dd68c;
--ms-mid: #d4a017;
--surface: rgba(18, 32, 28, 0.88);
--surface-2: rgba(24, 40, 35, 0.72);
--input-bg: rgba(12, 24, 20, 0.92);
--row-bg: rgba(22, 36, 31, 0.75);
--hero-bg: linear-gradient(160deg, rgba(22,38,33,.95), rgba(16,28,24,.9));
--hero-bg-on: linear-gradient(160deg, rgba(18,48,40,.96), rgba(22,38,33,.92));
--chip-bg: rgba(28, 44, 38, 0.85);
--server-list-bg: rgba(10, 20, 17, 0.55);
--server-row-bg: rgba(22, 36, 31, 0.85);
--server-row-hover: rgba(30, 48, 41, 0.95);
--server-row-active: linear-gradient(135deg, rgba(18,52,42,.95), rgba(24,40,35,.95));
--meta-bg: rgba(14, 26, 22, 0.55);
--meta-ok-bg: rgba(18, 52, 42, 0.65);
--meta-err-bg: rgba(56, 28, 26, 0.7);
--shop-bg: linear-gradient(135deg, rgba(46,201,149,.1), rgba(18,32,28,.7));
--shop-border: rgba(46, 201, 149, 0.2);
--modal-bg: #15251f;
--modal-scrim: rgba(4, 12, 10, 0.62);
--update-bg: linear-gradient(135deg, rgba(46,201,149,.16), rgba(18,32,28,.85));
--update-border: rgba(46, 201, 149, 0.28);
--icon-btn-bg: linear-gradient(180deg, #1e3d34, #17332b);
--icon-btn-border: rgba(46, 201, 149, 0.28);
--icon-btn-hover: linear-gradient(180deg, #255045, #1c3c33);
--mini-bg: linear-gradient(180deg, #1e3d34, #17332b);
--mini-accent-bg: linear-gradient(180deg, #1aa877, #0d8a66);
--mini-accent-fg: #f2fff9;
--switch-off: #3a4f47;
--switch-knob: #e8f5ef;
--dot-off: #5a736b;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
--muted: #8b9bb4;
--line: rgba(148, 163, 184, 0.14);
--accent: #22d3ee;
--accent-deep: #06b6d4;
--accent-soft: rgba(34, 211, 238, 0.12);
--danger: #f87171;
--ok: #2dd4bf;
--ms-good: #34d399;
--ms-mid: #fbbf24;
--surface: rgba(15, 20, 30, 0.92);
--surface-2: rgba(22, 30, 44, 0.88);
--input-bg: rgba(10, 14, 22, 0.95);
--row-bg: rgba(20, 28, 40, 0.8);
--hero-bg: linear-gradient(155deg, #121826 0%, #0f172a 55%, #111827 100%);
--hero-bg-on: linear-gradient(155deg, #0c2a32 0%, #132033 50%, #121826 100%);
--chip-bg: rgba(30, 41, 59, 0.9);
--server-list-bg: rgba(8, 12, 20, 0.55);
--server-row-bg: rgba(22, 30, 44, 0.9);
--server-row-hover: rgba(30, 41, 59, 0.95);
--server-row-active: linear-gradient(135deg, rgba(8,47,55,.95), rgba(22,30,44,.95));
--meta-bg: rgba(10, 14, 22, 0.7);
--meta-ok-bg: rgba(6, 78, 72, 0.45);
--meta-err-bg: rgba(69, 26, 26, 0.55);
--shop-bg: linear-gradient(135deg, rgba(34,211,238,.08), rgba(15,20,30,.85));
--shop-border: rgba(34, 211, 238, 0.18);
--modal-bg: #121826;
--modal-scrim: rgba(2, 6, 14, 0.72);
--update-bg: linear-gradient(135deg, rgba(34,211,238,.12), rgba(15,20,30,.9));
--update-border: rgba(34, 211, 238, 0.28);
--icon-btn-bg: linear-gradient(180deg, #1e293b, #162032);
--icon-btn-border: rgba(34, 211, 238, 0.22);
--icon-btn-hover: linear-gradient(180deg, #243247, #1a2536);
--mini-bg: linear-gradient(180deg, #1e293b, #162032);
--mini-accent-bg: linear-gradient(180deg, #0891b2, #0e7490);
--mini-accent-fg: #ecfeff;
--switch-off: #334155;
--switch-knob: #e2e8f0;
--dot-off: #64748b;
--shadow: 0 22px 55px rgba(0, 0, 0, 0.5);
--page-bg:
radial-gradient(720px 400px at 6% -8%, rgba(26, 90, 70, 0.45) 0%, transparent 55%),
radial-gradient(560px 340px at 100% 0%, rgba(28, 56, 72, 0.4) 0%, transparent 48%),
radial-gradient(480px 300px at 50% 110%, rgba(18, 64, 48, 0.35) 0%, transparent 45%),
linear-gradient(165deg, #0c1613 0%, #121f1a 48%, #101a1c 100%);
--theme-toggle-bg: rgba(28, 44, 38, 0.85);
radial-gradient(680px 380px at 5% -8%, rgba(8, 90, 110, 0.35) 0%, transparent 55%),
radial-gradient(520px 300px at 100% 0%, rgba(30, 58, 95, 0.32) 0%, transparent 48%),
linear-gradient(165deg, #070b12 0%, #0c1220 48%, #0a1018 100%);
--theme-toggle-bg: rgba(30, 41, 59, 0.85);
--sidebar-rail: rgba(34, 211, 238, 0.45);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
font-family: Figtree, sans-serif;
font-family: Manrope, sans-serif;
color: var(--ink);
background: var(--page-bg);
}
@@ -138,14 +139,24 @@
padding: 16px 12px 20px;
}
.shell {
width: min(480px, 100%);
width: min(500px, 100%);
background: var(--surface);
backdrop-filter: blur(18px) saturate(1.15);
border: 1px solid var(--line);
box-shadow: var(--shadow);
border-radius: 28px;
padding: 18px 18px 16px;
border-radius: 22px;
padding: 16px 16px 14px;
animation: rise .55s var(--ease) both;
position: relative;
overflow: hidden;
}
.shell::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
background: linear-gradient(180deg, var(--accent), transparent 70%);
opacity: .9;
}
@keyframes rise {
from { opacity: 0; transform: translateY(14px) scale(.985); }
@@ -163,7 +174,7 @@
}
.update-banner.show { display: block; }
.update-banner strong {
font-family: Sora, sans-serif;
font-family: Outfit, sans-serif;
display: block;
margin-bottom: 4px;
font-size: .95rem;
@@ -190,13 +201,13 @@
margin-bottom: 14px;
}
.logo {
width: 52px;
height: 52px;
border-radius: 16px;
width: 48px;
height: 48px;
border-radius: 14px;
overflow: hidden;
flex: 0 0 auto;
box-shadow: 0 10px 24px rgba(13, 138, 102, 0.28);
background: linear-gradient(145deg, #0d8a66, #1bb887);
box-shadow: 0 10px 24px rgba(8, 145, 178, 0.28);
background: linear-gradient(145deg, #0e7490, #22d3ee);
display: grid;
place-items: center;
}
@@ -209,7 +220,7 @@
flex-wrap: wrap;
}
.brand {
font-family: Sora, sans-serif;
font-family: Outfit, sans-serif;
font-size: 1.7rem;
font-weight: 800;
letter-spacing: -0.045em;
@@ -223,8 +234,8 @@
text-transform: uppercase;
color: var(--accent-deep);
background: var(--accent-soft);
border: 1px solid rgba(13,138,102,.18);
border-radius: 999px;
border: 1px solid rgba(8,145,178,.22);
border-radius: 8px;
padding: 3px 8px;
}
.tagline {
@@ -248,7 +259,7 @@
padding: 0;
}
.theme-toggle:hover {
border-color: rgba(13, 138, 102, 0.35);
border-color: rgba(8, 145, 178, 0.4);
transform: translateY(-1px);
}
.theme-toggle:active { transform: none; }
@@ -351,7 +362,7 @@
justify-content: space-between;
gap: 8px;
margin: 0 0 8px;
font-family: Sora, sans-serif;
font-family: Outfit, sans-serif;
font-size: .78rem;
font-weight: 700;
letter-spacing: .04em;
@@ -560,7 +571,7 @@
width: 100%;
min-height: 52px;
font-size: 1.02rem;
font-family: Sora, sans-serif;
font-family: Outfit, sans-serif;
}
.actions { display: grid; gap: 8px; }
@@ -642,15 +653,38 @@
color: inherit;
}
.server-row:hover {
border-color: rgba(13,138,102,.25);
border-color: rgba(8,145,178,.3);
background: var(--server-row-hover);
}
.server-row.active {
border-color: rgba(13,138,102,.4);
border-color: rgba(8,145,178,.45);
background: var(--server-row-active);
box-shadow: 0 4px 14px rgba(13,138,102,.1);
box-shadow: 0 4px 14px rgba(8,145,178,.12);
}
.server-row .left { min-width: 0; }
.server-row .left {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.server-row .flag {
flex: 0 0 auto;
width: 28px;
height: 28px;
border-radius: 8px;
display: grid;
place-items: center;
font-size: 1.1rem;
line-height: 1;
background: var(--chip-bg);
border: 1px solid var(--line);
}
.server-row .flag.unknown {
font-size: .7rem;
font-weight: 700;
color: var(--muted);
}
.server-row .meta-col { min-width: 0; }
.server-row .name {
font-weight: 700;
font-size: .84rem;
@@ -693,6 +727,27 @@
.server-row .ms.good { color: var(--ms-good); }
.server-row .ms.mid { color: var(--ms-mid); }
.server-row .ms.bad { color: var(--danger); }
.remna-box {
margin-top: 4px;
padding: 12px;
border-radius: 14px;
border: 1px solid var(--line);
background: var(--surface-2);
display: grid;
gap: 8px;
}
.remna-title {
font-family: Outfit, sans-serif;
font-weight: 700;
font-size: .92rem;
letter-spacing: -.02em;
}
.remna-hint {
margin: 0;
font-size: .75rem;
color: var(--muted);
line-height: 1.4;
}
.server-empty {
padding: 14px 10px;
text-align: center;
@@ -759,7 +814,7 @@
background: var(--shop-bg);
}
.shop h3 {
font-family: Sora, sans-serif;
font-family: Outfit, sans-serif;
font-size: 1rem;
letter-spacing: -0.02em;
margin: 0 0 5px;
@@ -815,7 +870,7 @@
animation: rise .35s var(--ease) both;
}
.modal h2 {
font-family: Sora, sans-serif;
font-family: Outfit, sans-serif;
margin: 0 0 12px;
font-size: 1.2rem;
letter-spacing: -.02em;
@@ -854,9 +909,9 @@
<div class="brand-wrap">
<div class="brand-row">
<h1 class="brand">Navis</h1>
<span class="badge-ver" id="badgeVer">3.8.2</span>
<span class="badge-ver" id="badgeVer">3.9.0</span>
</div>
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
<p class="tagline">Remnawave · Naive · Hy2 · AWG · Xray</p>
</div>
<button type="button" class="theme-toggle" id="themeToggle" title="Тема" aria-label="Переключить тему">
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
@@ -901,7 +956,7 @@
<select id="profile" hidden></select>
<details class="profile-edit" id="profileEdit">
<summary>Редактировать выбранный / подписка</summary>
<summary>Редактировать / подписка / Remnawave</summary>
<div class="stack" style="margin-top:8px">
<div>
<label class="field" for="name">Название</label>
@@ -912,12 +967,36 @@
<textarea id="proxy" rows="3" placeholder="vless:// · vmess:// · trojan:// · hy2:// · naive+https:// · AWG .conf" spellcheck="false"></textarea>
</div>
<div>
<label class="field" for="subUrl">Подписка</label>
<label class="field" for="subUrl">URL подписки</label>
<div class="sub-bar">
<input id="subUrl" type="text" placeholder="https://…/sub" spellcheck="false" />
<input id="subUrl" type="text" placeholder="https://…/api/sub/… или обычная подписка" spellcheck="false" />
<button class="icon-btn wide" id="subBtn" type="button" title="Обновить подписку">Обновить</button>
</div>
</div>
<div class="remna-box">
<div class="remna-title">Remnawave API</div>
<p class="remna-hint">Панель → short UUID пользователя. Токен API — опционально (для /api/subscriptions/… и /users/…).</p>
<div>
<label class="field" for="rwBase">URL панели</label>
<input id="rwBase" type="text" placeholder="https://panel.example.com" spellcheck="false" />
</div>
<div>
<label class="field" for="rwShort">Short UUID</label>
<input id="rwShort" type="text" placeholder="abc12xyz" spellcheck="false" />
</div>
<div>
<label class="field" for="rwToken">API token</label>
<input id="rwToken" type="password" placeholder="Bearer из панели → API Tokens" spellcheck="false" autocomplete="off" />
</div>
<div>
<label class="field" for="rwCaddy">Caddy X-Api-Key</label>
<input id="rwCaddy" type="password" placeholder="если стоит auth у Caddy" spellcheck="false" autocomplete="off" />
</div>
<div class="sub-bar" style="margin-top:8px">
<button class="action secondary" id="rwSaveBtn" type="button" style="flex:1">Сохранить</button>
<button class="action primary" id="rwSyncBtn" type="button" style="flex:1">Синхронизировать</button>
</div>
</div>
<button class="action secondary" id="saveBtn" type="button">Сохранить профиль</button>
</div>
</details>
@@ -1034,7 +1113,7 @@
const methods = [
"getState","connect","disconnect","connectProfile","saveProfile","createProfile",
"selectProfile","deleteProfile","installCore","openURL","pingServers","pingBest",
"checkUpdate","applyUpdate","saveHy2","importSubscription","quit"
"checkUpdate","applyUpdate","saveHy2","importSubscription","saveRemnawave","importRemnawave","quit"
];
if (typeof window.getState === "function") return;
window.__navisHttp = true;
@@ -1094,7 +1173,7 @@
try {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
} catch (_) {
return "light";
return "dark";
}
}
function applyTheme(t) {
@@ -1136,6 +1215,12 @@
}
const subUrl = $("subUrl");
const subBtn = $("subBtn");
const rwBase = $("rwBase");
const rwShort = $("rwShort");
const rwToken = $("rwToken");
const rwCaddy = $("rwCaddy");
const rwSaveBtn = $("rwSaveBtn");
const rwSyncBtn = $("rwSyncBtn");
const hero = $("hero");
const protoChip = $("protoChip");
const heroHint = $("heroHint");
@@ -1201,7 +1286,7 @@
function markDirty() { dirty = true; }
[
proxy, nameInput, sysproxy, subUrl,
proxy, nameInput, sysproxy, subUrl, rwBase, rwShort, rwToken, rwCaddy,
$("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"),
$("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"),
$("hy2Insecure"), $("hy2Fast"), $("hy2Lazy")
@@ -1291,14 +1376,22 @@
const left = document.createElement("div");
left.className = "left";
const flag = document.createElement("span");
flag.className = "flag" + (p.flag ? "" : " unknown");
flag.textContent = p.flag || (p.country || "·");
flag.title = p.country || "регион неизвестен";
const metaCol = document.createElement("div");
metaCol.className = "meta-col";
const nameEl = document.createElement("div");
nameEl.className = "name";
nameEl.textContent = p.name;
const sub = document.createElement("div");
sub.className = "sub";
sub.textContent = p.host || "нет хоста";
left.appendChild(nameEl);
left.appendChild(sub);
metaCol.appendChild(nameEl);
metaCol.appendChild(sub);
left.appendChild(flag);
left.appendChild(metaCol);
const right = document.createElement("div");
right.className = "right";
@@ -1413,6 +1506,12 @@
updateBtn.disabled = busy;
subBtn.disabled = busy;
subUrl.disabled = busy;
if (rwBase) rwBase.disabled = busy;
if (rwShort) rwShort.disabled = busy;
if (rwToken) rwToken.disabled = busy;
if (rwCaddy) rwCaddy.disabled = busy;
if (rwSaveBtn) rwSaveBtn.disabled = busy;
if (rwSyncBtn) rwSyncBtn.disabled = busy;
btn.disabled = busy;
rememberPings(state.pings || []);
@@ -1421,6 +1520,12 @@
if (typeof state.subscription_url === "string" && !dirty) {
subUrl.value = state.subscription_url;
}
if (state.remnawave && !dirty) {
rwBase.value = state.remnawave.base_url || "";
rwShort.value = state.remnawave.short_uuid || "";
if (typeof state.remnawave.token === "string") rwToken.value = state.remnawave.token;
if (typeof state.remnawave.caddy_token === "string") rwCaddy.value = state.remnawave.caddy_token;
}
if (syncForm || (!formHydrated && !dirty)) {
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
@@ -1484,7 +1589,7 @@
}
function paintButtonsLocked(locked) {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, updateBtn, skipUpdateBtn, subBtn, subUrl].forEach((b) => {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, updateBtn, skipUpdateBtn, subBtn, subUrl, rwBase, rwShort, rwToken, rwCaddy, rwSaveBtn, rwSyncBtn].forEach((b) => {
if (b) b.disabled = locked;
});
}
@@ -1601,6 +1706,15 @@
await runBest(!!autoBest.checked);
}
function readRemnawave() {
return {
base_url: (rwBase && rwBase.value || "").trim(),
short_uuid: (rwShort && rwShort.value || "").trim(),
token: (rwToken && rwToken.value || "").trim(),
caddy_token: (rwCaddy && rwCaddy.value || "").trim()
};
}
subBtn.addEventListener("click", () => withBusy(async () => {
try {
await runImportSubscription();
@@ -1613,6 +1727,32 @@
}
});
if (rwSaveBtn) {
rwSaveBtn.addEventListener("click", () => withBusy(async () => {
try {
await saveRemnawave(readRemnawave());
setMeta("Настройки Remnawave сохранены", "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
}
if (rwSyncBtn) {
rwSyncBtn.addEventListener("click", () => withBusy(async () => {
try {
const s = readRemnawave();
if (!s.base_url || !s.short_uuid) {
setMeta("Укажите URL панели и short UUID", "err");
return;
}
setMeta("Remnawave: загрузка конфигов…");
const n = await importRemnawave(s);
formHydrated = false;
dirty = false;
setMeta("Remnawave: импортировано " + n + " · пинг…", "ok");
await runBest(!!autoBest.checked);
} catch (e) { setMeta(String(e), "err"); }
}));
}
pingBtn.addEventListener("click", () => withBusy(async () => {
try {
setMeta("Пинг серверов…");