From f657644ce6b0200eee0ff9732d98b567b9ff46b2 Mon Sep 17 00:00:00 2001 From: savsis Date: Thu, 10 Sep 2026 18:21:13 +0500 Subject: [PATCH] admin: full node editing (label/address/port/sni/keys), country picker in edit modal Co-Authored-By: Claude Sonnet 5 --- admin.html | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++- api.py | 5 ++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/admin.html b/admin.html index 5368989..1f3af6f 100644 --- a/admin.html +++ b/admin.html @@ -217,9 +217,29 @@ .empty { text-align: center; color: var(--muted); padding: 40px 0; font-size: 13px; } + .modal-overlay { + position: fixed; inset: 0; background: rgba(1,4,9,0.6); backdrop-filter: blur(2px); + display: flex; align-items: center; justify-content: center; padding: 24px; z-index: 100; + opacity: 0; pointer-events: none; transition: opacity 0.18s var(--ease); + } + .modal-overlay.show { opacity: 1; pointer-events: auto; } + .modal-card { + width: 100%; max-width: 480px; max-height: 88vh; overflow-y: auto; + background: #161b22; border: 1px solid var(--border-strong); border-radius: 14px; padding: 22px; + box-shadow: 0 24px 64px rgba(0,0,0,0.5); + opacity: 0; transform: translateY(10px) scale(0.98); filter: blur(4px); + transition: opacity 0.2s var(--ease), transform 0.2s var(--ease), filter 0.2s var(--ease); + } + .modal-overlay.show .modal-card { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } + .modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } + .modal-head h2 { font-size: 16px; margin: 0; font-weight: 600; } + .modal-close { background: none; border: none; color: var(--muted); font-size: 22px; line-height: 1; cursor: pointer; padding: 2px 6px; border-radius: 6px; } + .modal-close:hover { color: var(--text); background: var(--card2-tint); } + .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; } + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } - .reveal, .login-card, .splash-mark, .splash-title, .splash-tagline { opacity: 1 !important; transform: none !important; filter: none !important; } + .reveal, .login-card, .splash-mark, .splash-title, .splash-tagline, .modal-overlay, .modal-card { opacity: 1 !important; transform: none !important; filter: none !important; } } @@ -354,6 +374,43 @@ + +
Трафик
Суммарно по всем нодам, live через Xray Stats API
@@ -572,6 +629,13 @@ function initDropdowns() { }); DD.giftNode = createDropdown("gift-node", { options: [], placeholder: "Сервер" }); DD.giftPlan = createDropdown("gift-plan", { options: [], placeholder: "Срок" }); + DD.editCountry = createDropdown("edit-country", { + options: countryOptions, placeholder: "Страна", searchable: true, + onChange: (code) => { + const c = COUNTRIES.find((x) => x[0] === code); + if (c) document.getElementById("edit-label").value = `${flagEmoji(code)} ${c[1]}`; + }, + }); } const ICONS = { @@ -694,6 +758,7 @@ async function loadNodes() { ${n.status === "pending" ? 'ожидает установки' : (n.enabled ? 'включена' : 'выключена')} ${n.status === "pending" ? "—" : ``} + ${n.code !== "de1" ? `` : ""} ${n.code !== "de1" ? `` : ""} @@ -714,6 +779,52 @@ async function loadNodeMetrics(code) { } } +let editingNodeCode = null; +function openEditNode(code) { + const n = nodesCache.find((x) => x.code === code); + if (!n) return; + editingNodeCode = code; + const isDe1 = code === "de1"; + document.getElementById("edit-node-title").textContent = `Редактировать: ${n.label}`; + document.getElementById("edit-label").value = n.label || ""; + DD.editCountry.setValue(null); + document.getElementById("edit-node-advanced").style.display = isDe1 ? "none" : "block"; + document.getElementById("edit-node-de1-note").style.display = isDe1 ? "block" : "none"; + document.getElementById("edit-address").value = n.address || ""; + document.getElementById("edit-port").value = n.port || 443; + document.getElementById("edit-sni").value = n.sni || ""; + document.getElementById("edit-flow").value = n.flow || ""; + document.getElementById("edit-pbk").value = n.public_key || ""; + document.getElementById("edit-sid").value = n.short_id || ""; + document.getElementById("edit-uuid").value = n.shared_uuid || ""; + document.getElementById("edit-node-err").textContent = ""; + document.getElementById("edit-node-overlay").classList.add("show"); +} +function closeEditNode() { + document.getElementById("edit-node-overlay").classList.remove("show"); +} +async function saveEditNode() { + const code = editingNodeCode; + if (!code) return; + const body = { label: document.getElementById("edit-label").value.trim() }; + if (code !== "de1") { + body.address = document.getElementById("edit-address").value.trim(); + body.port = parseInt(document.getElementById("edit-port").value || "443"); + body.sni = document.getElementById("edit-sni").value.trim(); + body.flow = document.getElementById("edit-flow").value.trim(); + body.public_key = document.getElementById("edit-pbk").value.trim(); + body.short_id = document.getElementById("edit-sid").value.trim(); + body.shared_uuid = document.getElementById("edit-uuid").value.trim() || null; + } + try { + await api(`/admin/api/nodes/${code}`, { method: "PATCH", body: JSON.stringify(body) }); + closeEditNode(); + loadNodes(); + } catch (e) { + document.getElementById("edit-node-err").innerHTML = '

Не удалось сохранить

'; + } +} + async function toggleNode(code, enabled) { await api(`/admin/api/nodes/${code}`, { method: "PATCH", body: JSON.stringify({ enabled }) }); loadNodes(); diff --git a/api.py b/api.py index 12494f3..e9b5031 100644 --- a/api.py +++ b/api.py @@ -444,7 +444,10 @@ def admin_create_node(request: Request, body: dict = Body(...)): @app.patch("/admin/api/nodes/{code}") def admin_update_node(code: str, request: Request, body: dict = Body(...)): require_admin(request) - allowed = {k: v for k, v in body.items() if k in {"label", "enabled", "address", "port", "sni"}} + editable = {"label", "enabled", "address", "port", "sni", "public_key", "short_id", "flow", "shared_uuid"} + if code == "de1": + editable = {"label"} + allowed = {k: v for k, v in body.items() if k in editable} return db.update_node(code, **allowed)