Release 4.0.1+1: remove account binding / centralized provisioning (no panel credentials in binary; configs via user's own subscription URL); add recommended-services block on About page; Windows 4.0.1.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-08-01 20:31:29 +03:00
co-authored by Cursor
parent 1144fab54d
commit 5f768c7732
26 changed files with 237 additions and 1255 deletions
+83 -81
View File
@@ -473,7 +473,7 @@
.cur-server .ms.mid { color: var(--ms-mid); }
.cur-server .ms.bad { color: var(--danger); }
/* Account / subscription cards */
/* Subscription card */
.subinfo {
border: 1px solid var(--line);
border-radius: var(--radius);
@@ -936,6 +936,43 @@
}
.shop-link:hover { text-decoration: underline; }
/* «Рекомендуемые сервисы» — маленький ненавязчивый блок в углу «О программе» */
.svc {
margin-top: 12px;
padding: 10px 12px 11px;
border-radius: 14px;
border: 1px dashed var(--line);
background: transparent;
}
.svc h4 {
margin: 0 0 7px;
font-family: Outfit, sans-serif;
font-size: .66rem;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--muted);
}
.svc-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.svc-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--muted);
font: inherit;
font-size: .74rem;
font-weight: 600;
cursor: pointer;
box-shadow: none;
transition: color .15s, border-color .15s;
}
.svc-link:hover { color: var(--accent-deep); border-color: rgba(13,138,102,.45); }
.svc-link .ic { font-size: .85rem; line-height: 1; }
.about-card {
border: 1px solid var(--line);
border-radius: var(--radius);
@@ -1077,25 +1114,6 @@
</span>
</button>
<!-- Аккаунт: показывается, когда пользователь «запомнен» (configs/account.json) -->
<section class="subinfo" id="accountBox" hidden>
<div class="subinfo-title">Аккаунт</div>
<div class="subinfo-row">
<span class="k">Пользователь</span>
<span class="v" id="accName"></span>
</div>
<div class="subinfo-row" id="accExpireRow" hidden>
<span class="k">Действует до</span>
<span class="v" id="accExpire"></span>
</div>
<div class="subinfo-row" id="accTrafficRow" hidden>
<span class="k">Тариф</span>
<span class="v" id="accTraffic"></span>
</div>
<button class="action primary" id="renewBtn" type="button">Продлить 50 ГБ</button>
<p class="remna-hint" style="margin-top:2px">Продление сбрасывает трафик и добавляет 30 дней тому же пользователю. Также продлевается автоматически при истечении.</p>
</section>
<section class="subinfo" id="subInfoCard" hidden>
<div class="subinfo-title">Подписка</div>
<div class="subinfo-row" id="subExpireRow" hidden>
@@ -1292,6 +1310,12 @@
<button class="action primary" id="shopBtn" type="button">Открыть магазин</button>
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
</section>
<!-- Рекомендуемые сервисы: ненавязчивый блок в нижнем углу «О программе».
Ссылки задаются в скрипте — константа RECOMMENDED_SERVICES. -->
<section class="svc" aria-label="Рекомендуемые сервисы">
<h4>Рекомендуемые сервисы</h4>
<div class="svc-grid" id="svcGrid"></div>
</section>
<button class="shop-link" id="quitBtn" type="button" hidden style="display:none;margin:12px auto 0;width:auto">Выйти</button>
</section>
</div>
@@ -1327,7 +1351,7 @@
"getState","connect","disconnect","connectProfile","saveProfile","createProfile",
"selectProfile","deleteProfile","installCore","openURL","pingServers","pingBest",
"checkUpdate","applyUpdate","saveHy2","importSubscription","saveRemnawave","importRemnawave",
"provisionUser","renewAccount","setMode","getLogs","quit"
"setMode","getLogs","quit"
];
if (typeof window.getState === "function") return;
window.__navisHttp = true;
@@ -1378,8 +1402,6 @@
const storeUpdateNote = $("storeUpdateNote");
const themeToggle = $("themeToggle");
const themeSwitch = $("themeSwitch");
const renewBtn = $("renewBtn");
const accountBox = $("accountBox");
const modeProxyBtn = $("modeProxyBtn");
const modeVpnBtn = $("modeVpnBtn");
const modeSection = $("modeSection");
@@ -1464,6 +1486,15 @@
const protoChip = $("protoChip");
const heroHint = $("heroHint");
const SHOP_URL = "https://evilfox.win/";
// Список рекомендуемых сервисов — замените ссылки/названия здесь.
// Хост каждой ссылки должен быть разрешён в allowlist openURL
// (internal/apphost/app.go → allowedLinkHosts), иначе ссылка не откроется.
const RECOMMENDED_SERVICES = [
{ icon: "🛒", name: "EvilFox Shop", url: "https://evilfox.win/" },
{ icon: "✈️", name: "Telegram", url: "https://t.me/evilfox" },
{ icon: "🖥️", name: "Хостинг de4ima", url: "https://de4ima.uk" },
{ icon: "🦊", name: "Git", url: "https://git.de4ima.uk/Evilfox/navi" }
];
const AUTO_BEST_KEY = "navis.autoBest";
try {
@@ -1736,44 +1767,6 @@
}
}
function renderAccount(acc, subInfo) {
if (!accountBox) return;
if (!acc) {
accountBox.hidden = true;
return;
}
accountBox.hidden = false;
$("accName").textContent = acc.username || "—";
const expRow = $("accExpireRow");
if (acc.expire_at) {
expRow.hidden = false;
const daysLeft = Math.floor((acc.expire_at * 1000 - Date.now()) / 86400000);
const el = $("accExpire");
if (daysLeft < 0) {
el.textContent = fmtDate(acc.expire_at) + " · истёк";
el.classList.add("warn");
} else {
el.textContent = fmtDate(acc.expire_at) + " · осталось " + daysLeft + " дн.";
el.classList.toggle("warn", daysLeft <= 3);
}
} else {
expRow.hidden = true;
}
const trafRow = $("accTrafficRow");
const limit = acc.traffic_limit_bytes || 0;
const used = subInfo ? ((subInfo.upload || 0) + (subInfo.download || 0)) : 0;
if (limit > 0 || used > 0) {
trafRow.hidden = false;
$("accTraffic").textContent = limit > 0
? (fmtTraffic(used) + " из " + fmtTraffic(limit))
: (fmtTraffic(used) + " · Безлимит");
} else {
trafRow.hidden = true;
}
}
function renderMode(state) {
currentMode = state.mode === "vpn" ? "vpn" : "proxy";
if (modeSection) {
@@ -1894,7 +1887,6 @@
updateBtn.disabled = busy;
subBtn.disabled = busy;
subUrl.disabled = busy;
if (renewBtn) renewBtn.disabled = busy || !state.provision_ready;
if (modeProxyBtn) modeProxyBtn.disabled = busy || connected;
if (modeVpnBtn) modeVpnBtn.disabled = busy || connected;
btn.disabled = busy;
@@ -1903,7 +1895,6 @@
fillProfiles(state.profiles || [], state.active_profile || state.profile);
renderUpdate(state.update, state.version);
renderSubInfo(state.sub_info);
renderAccount(state.account, state.sub_info);
renderMode(state);
if (typeof state.subscription_url === "string" && !dirty) {
subUrl.value = state.subscription_url;
@@ -1975,7 +1966,7 @@
function paintButtonsLocked(locked) {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, aboutUpdBtn,
updateBtn, skipUpdateBtn, subBtn, subUrl, renewBtn, modeProxyBtn, modeVpnBtn, logsRefreshBtn].forEach((b) => {
updateBtn, skipUpdateBtn, subBtn, subUrl, modeProxyBtn, modeVpnBtn, logsRefreshBtn].forEach((b) => {
if (b) b.disabled = locked;
});
}
@@ -2079,6 +2070,34 @@
shopBtn.addEventListener("click", openShop);
shopLink.addEventListener("click", openShop);
// «Рекомендуемые сервисы» — рендер из RECOMMENDED_SERVICES (см. константу выше).
(function renderRecommendedServices() {
const grid = $("svcGrid");
if (!grid) return;
RECOMMENDED_SERVICES.forEach((s) => {
const b = document.createElement("button");
b.type = "button";
b.className = "svc-link";
b.title = s.url;
const ic = document.createElement("span");
ic.className = "ic";
ic.textContent = s.icon || "•";
const nm = document.createElement("span");
nm.textContent = s.name;
b.append(ic, nm);
b.addEventListener("click", async () => {
try {
setMeta("Открываю " + s.name + "…");
await openURL(s.url);
setMeta("Открыто в браузере", "ok");
} catch (err) {
setMeta("Не удалось открыть ссылку: " + String(err), "err");
}
});
grid.appendChild(b);
});
})();
async function runImportSubscription() {
const url = subUrl.value.trim();
if (!url) {
@@ -2114,23 +2133,6 @@
}
});
if (renewBtn) {
renewBtn.addEventListener("click", () => withBusy(async () => {
try {
setMeta("Продление тарифа (50 ГБ / +30 дней)…");
const r = await renewAccount();
formHydrated = false;
dirty = false;
let msg = "Продлено: " + 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 (_) {}
}
setMeta(msg, "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
}
async function switchMode(mode) {
if (mode === currentMode) return;
await withBusy(async () => {