Release 3.9.0+2: tolerant subscription import, full Remnawave server list, subscription info card (expiry / traffic / devices).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+120
-4
@@ -507,6 +507,35 @@
|
||||
font-weight: 600;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.subinfo {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--row-bg);
|
||||
padding: 11px 13px;
|
||||
margin-bottom: 12px;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
.subinfo-title {
|
||||
font-family: Outfit, sans-serif;
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .05em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.subinfo-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: .84rem;
|
||||
}
|
||||
.subinfo-row .k { color: var(--muted); flex: 0 0 auto; }
|
||||
.subinfo-row .v { font-weight: 700; text-align: right; min-width: 0; }
|
||||
.subinfo-row .v.warn { color: #dc2626; }
|
||||
[data-theme="dark"] .subinfo-row .v.warn { color: #f87171; }
|
||||
|
||||
.switch { position: relative; width: 44px; height: 26px; flex: 0 0 auto; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider {
|
||||
@@ -1002,6 +1031,22 @@
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="subinfo" id="subInfoCard" hidden>
|
||||
<div class="subinfo-title">Подписка</div>
|
||||
<div class="subinfo-row" id="subExpireRow" hidden>
|
||||
<span class="k">Окончание подписки</span>
|
||||
<span class="v" id="subExpireVal"></span>
|
||||
</div>
|
||||
<div class="subinfo-row" id="subTrafficRow" hidden>
|
||||
<span class="k">Трафик</span>
|
||||
<span class="v" id="subTrafficVal"></span>
|
||||
</div>
|
||||
<div class="subinfo-row" id="subDevicesRow" hidden>
|
||||
<span class="k">Устройства</span>
|
||||
<span class="v" id="subDevicesVal"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row">
|
||||
<span>Системный прокси</span>
|
||||
<label class="switch">
|
||||
@@ -1444,6 +1489,67 @@
|
||||
});
|
||||
}
|
||||
|
||||
function fmtTraffic(bytes) {
|
||||
if (!bytes || bytes <= 0) return "0 ГБ";
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
if (gb >= 1024) {
|
||||
const tb = gb / 1024;
|
||||
return (tb >= 100 ? Math.round(tb) : tb.toFixed(1)) + " ТБ";
|
||||
}
|
||||
if (gb >= 100) return Math.round(gb) + " ГБ";
|
||||
if (gb >= 1) return gb.toFixed(1) + " ГБ";
|
||||
const mb = bytes / (1024 * 1024);
|
||||
return (mb >= 1 ? Math.round(mb) : 1) + " МБ";
|
||||
}
|
||||
|
||||
function renderSubInfo(info) {
|
||||
const card = $("subInfoCard");
|
||||
if (!card) return;
|
||||
const expireRow = $("subExpireRow"), trafficRow = $("subTrafficRow"), devicesRow = $("subDevicesRow");
|
||||
const hasExpire = !!(info && info.expire);
|
||||
const hasTraffic = !!(info && (info.total || info.download || info.upload));
|
||||
const hasDevices = !!(info && info.devices_known);
|
||||
if (!info || (!hasExpire && !hasTraffic && !hasDevices)) {
|
||||
card.hidden = true;
|
||||
return;
|
||||
}
|
||||
card.hidden = false;
|
||||
|
||||
expireRow.hidden = !hasExpire;
|
||||
if (hasExpire) {
|
||||
const d = new Date(info.expire * 1000);
|
||||
let text = "";
|
||||
try {
|
||||
text = d.toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" });
|
||||
} catch (_) {
|
||||
text = d.toISOString().slice(0, 10);
|
||||
}
|
||||
const daysLeft = Math.floor((d.getTime() - Date.now()) / 86400000);
|
||||
const v = $("subExpireVal");
|
||||
if (daysLeft < 0) {
|
||||
v.textContent = text + " · истекла";
|
||||
v.classList.add("warn");
|
||||
} else {
|
||||
v.textContent = daysLeft <= 14 ? (text + " · осталось " + daysLeft + " дн.") : text;
|
||||
v.classList.toggle("warn", daysLeft <= 3);
|
||||
}
|
||||
}
|
||||
|
||||
trafficRow.hidden = !hasTraffic;
|
||||
if (hasTraffic) {
|
||||
const used = (info.upload || 0) + (info.download || 0);
|
||||
$("subTrafficVal").textContent = info.total > 0
|
||||
? (fmtTraffic(used) + " из " + fmtTraffic(info.total))
|
||||
: (fmtTraffic(used) + " · Безлимит");
|
||||
}
|
||||
|
||||
devicesRow.hidden = !hasDevices;
|
||||
if (hasDevices) {
|
||||
const n = info.devices || 0;
|
||||
$("subDevicesVal").textContent = info.device_limit > 0 ? (n + " из " + info.device_limit) : String(n);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUpdate(u, version) {
|
||||
const label = "Navis v" + (version || "?");
|
||||
verLabel.textContent = label;
|
||||
@@ -1517,6 +1623,7 @@
|
||||
rememberPings(state.pings || []);
|
||||
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
||||
renderUpdate(state.update, state.version);
|
||||
renderSubInfo(state.sub_info);
|
||||
if (typeof state.subscription_url === "string" && !dirty) {
|
||||
subUrl.value = state.subscription_url;
|
||||
}
|
||||
@@ -1699,13 +1806,22 @@
|
||||
return;
|
||||
}
|
||||
setMeta("Загрузка подписки…");
|
||||
const n = await importSubscription(url);
|
||||
const r = await importSubscription(url);
|
||||
formHydrated = false;
|
||||
dirty = false;
|
||||
setMeta("Импортировано: " + n + " · измеряю пинг…", "ok");
|
||||
setMeta(importSummary(r) + " · измеряю пинг…", "ok");
|
||||
await runBest(!!autoBest.checked);
|
||||
}
|
||||
|
||||
function importSummary(r) {
|
||||
if (r && typeof r === "object") {
|
||||
let s = "Импортировано " + (r.imported || 0) + " серверов";
|
||||
if (r.skipped > 0) s += " (пропущено " + r.skipped + ")";
|
||||
return s;
|
||||
}
|
||||
return "Импортировано: " + r;
|
||||
}
|
||||
|
||||
function readRemnawave() {
|
||||
return {
|
||||
base_url: (rwBase && rwBase.value || "").trim(),
|
||||
@@ -1744,10 +1860,10 @@
|
||||
return;
|
||||
}
|
||||
setMeta("Remnawave: загрузка конфигов…");
|
||||
const n = await importRemnawave(s);
|
||||
const r = await importRemnawave(s);
|
||||
formHydrated = false;
|
||||
dirty = false;
|
||||
setMeta("Remnawave: импортировано " + n + " · пинг…", "ok");
|
||||
setMeta("Remnawave: " + importSummary(r).toLowerCase() + " · пинг…", "ok");
|
||||
await runBest(!!autoBest.checked);
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user