Release 3.9.0+3: subscription-URL routing, emoji flags, Remnawave provisioning.

Route credential-less http(s) URLs pasted into the add-link field to
subscription import (fixes remaining 'proxy URI missing username').
Extend geoflag with RU country names and city hints; live Remnawave
names already carrying emoji flags are kept as-is. Add admin
provisioning via configs/remnawave-api.json (GET by-username / POST
users, 50 GB MONTH plan) and the «Выдать доступ» UI panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-08-01 17:36:54 +03:00
co-authored by Cursor
parent e34312ef9c
commit 77bd7da861
24 changed files with 974 additions and 110 deletions
+46 -2
View File
@@ -1026,6 +1026,15 @@
<button class="action primary" id="rwSyncBtn" type="button" style="flex:1">Синхронизировать</button>
</div>
</div>
<div class="remna-box" id="provisionBox" hidden>
<div class="remna-title">Выдать доступ</div>
<p class="remna-hint">Создаёт пользователя в панели Remnawave (тариф из configs/remnawave-api.json, по умолчанию 50 ГБ / 30 дней) и сразу импортирует его конфиги.</p>
<div>
<label class="field" for="provUser">Пользователь</label>
<input id="provUser" type="text" placeholder="username / @telegram / email" spellcheck="false" autocomplete="off" />
</div>
<button class="action primary" id="provBtn" type="button">Выдать доступ</button>
</div>
<button class="action secondary" id="saveBtn" type="button">Сохранить профиль</button>
</div>
</details>
@@ -1158,7 +1167,7 @@
const methods = [
"getState","connect","disconnect","connectProfile","saveProfile","createProfile",
"selectProfile","deleteProfile","installCore","openURL","pingServers","pingBest",
"checkUpdate","applyUpdate","saveHy2","importSubscription","saveRemnawave","importRemnawave","quit"
"checkUpdate","applyUpdate","saveHy2","importSubscription","saveRemnawave","importRemnawave","provisionUser","quit"
];
if (typeof window.getState === "function") return;
window.__navisHttp = true;
@@ -1266,6 +1275,9 @@
const rwCaddy = $("rwCaddy");
const rwSaveBtn = $("rwSaveBtn");
const rwSyncBtn = $("rwSyncBtn");
const provisionBox = $("provisionBox");
const provUser = $("provUser");
const provBtn = $("provBtn");
const hero = $("hero");
const protoChip = $("protoChip");
const heroHint = $("heroHint");
@@ -1618,6 +1630,9 @@
if (rwCaddy) rwCaddy.disabled = busy;
if (rwSaveBtn) rwSaveBtn.disabled = busy;
if (rwSyncBtn) rwSyncBtn.disabled = busy;
if (provisionBox) provisionBox.hidden = !state.provision_ready;
if (provUser) provUser.disabled = busy;
if (provBtn) provBtn.disabled = busy;
btn.disabled = busy;
rememberPings(state.pings || []);
@@ -1696,7 +1711,7 @@
}
function paintButtonsLocked(locked) {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, updateBtn, skipUpdateBtn, subBtn, subUrl, rwBase, rwShort, rwToken, rwCaddy, rwSaveBtn, rwSyncBtn].forEach((b) => {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, updateBtn, skipUpdateBtn, subBtn, subUrl, rwBase, rwShort, rwToken, rwCaddy, rwSaveBtn, rwSyncBtn, provUser, provBtn].forEach((b) => {
if (b) b.disabled = locked;
});
}
@@ -1869,6 +1884,35 @@
}));
}
if (provBtn) {
provBtn.addEventListener("click", () => withBusy(async () => {
try {
const u = (provUser.value || "").trim();
if (!u) {
setMeta("Укажите имя пользователя (username / @telegram / email)", "err");
return;
}
setMeta("Выдача доступа через панель…");
const r = await provisionUser(u);
formHydrated = false;
dirty = false;
let msg = (r.created ? "Создан пользователь " : "Найден пользователь ") + r.username;
if (r.traffic_limit_bytes > 0) msg += " · " + fmtTraffic(r.traffic_limit_bytes);
if (r.expire_at) {
try { msg += " · до " + new Date(r.expire_at * 1000).toLocaleDateString("ru-RU"); } catch (_) {}
}
msg += " · импортировано серверов: " + (r.imported || 0);
setMeta(msg, "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
provUser.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
provBtn.click();
}
});
}
pingBtn.addEventListener("click", () => withBusy(async () => {
try {
setMeta("Пинг серверов…");