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)