v2.5.0: harden admin key and traffic flows

This commit is contained in:
Виталий Литвинов
2026-04-25 12:28:33 +03:00
parent d74b05ccf8
commit 5225811b3c
6 changed files with 213 additions and 75 deletions

View File

@@ -142,6 +142,8 @@ const i18n = {
port443NoListeners: "No 443 listeners found",
port443Listeners: "listeners",
port443Error: "Port check failed",
port443Public: "public",
port443Configured: "telemt: {port}",
roleMtproxy: "MTProxy",
roleSite: "Website",
roleXray: "Xray / 3x-ui",
@@ -322,6 +324,8 @@ const i18n = {
port443NoListeners: "Слушателей 443 не найдено",
port443Listeners: "слушателей",
port443Error: "Проверка порта не удалась",
port443Public: "публичный",
port443Configured: "telemt: {port}",
roleMtproxy: "MTProxy",
roleSite: "Сайт",
roleXray: "Xray / 3x-ui",
@@ -374,6 +378,7 @@ const state = {
theme: document.documentElement.dataset.theme || "light",
trafficRange: "1h",
trafficView: "chart",
trafficLoading: false,
pendingUsers: new Set(),
};
@@ -634,6 +639,9 @@ function renderPort443(payload = {}) {
const listeners = Array.isArray(payload.listeners) ? payload.listeners : [];
const summary = $("#port443Summary");
const list = $("#port443List");
const configuredPort = Number(payload.configured_port) || 443;
$("#port443Number").textContent = "443";
$("#port443Configured").textContent = configuredPort === 443 ? t("port443Public") : t("port443Configured").replace("{port}", configuredPort);
if (payload.error) {
summary.textContent = t("port443Error");
summary.className = "port-status error";
@@ -760,14 +768,21 @@ function fallbackTrafficSummaries(rows) {
return {
range,
points: windowRows.length,
proxy_delta: Math.max(0, (Number(last.proxy_bytes) || 0) - (Number(first.proxy_bytes) || 0)),
site_delta: Math.max(0, (Number(last.site_bytes) || 0) - (Number(first.site_bytes) || 0)),
proxy_delta: windowRows.reduce((sum, item) => sum + Math.max(0, Number(item.proxy_delta) || 0), 0),
site_delta: windowRows.reduce((sum, item) => sum + Math.max(0, Number(item.site_delta) || 0), 0),
proxy_total: Number(last.proxy_bytes) || 0,
site_total: Number(last.site_bytes) || 0,
};
});
}
function renderTrafficLoading() {
$("#trafficChart").classList.toggle("is-hidden", state.trafficView !== "chart");
$("#trafficTableWrap").classList.toggle("is-hidden", state.trafficView !== "table");
$("#trafficChart").innerHTML = `<div class="empty-chart"><strong>${escapeHtml(t("loading"))}</strong></div>`;
$("#historyTable").innerHTML = `<tr><td colspan="5" class="empty-cell">${escapeHtml(t("loading"))}</td></tr>`;
}
function renderStats() {
const payload = statsPayload();
const status = payload.status || {};
@@ -784,6 +799,10 @@ function renderStats() {
$("#metricProxyTraffic").textContent = fmtBytes(stats.proxy_bytes);
$("#metricSiteTraffic").textContent = fmtBytes(stats.site_bytes);
updateTrafficControls();
if (state.trafficLoading) {
renderTrafficLoading();
return;
}
$("#trafficChart").classList.toggle("is-hidden", state.trafficView !== "chart");
$("#trafficTableWrap").classList.toggle("is-hidden", state.trafficView !== "table");
drawTrafficChart(historyRows);
@@ -948,6 +967,9 @@ async function refreshAll() {
}
renderOverview();
renderUsers();
if (state.page === "traffic") {
await refreshStats();
}
} catch (err) {
toast(err.message);
} finally {
@@ -955,10 +977,39 @@ async function refreshAll() {
}
}
async function refreshStats() {
const data = await api(`/api/stats?range=${encodeURIComponent(state.trafficRange)}`);
state.stats = data;
renderStats();
async function refreshUsers() {
state.users = await api("/api/users");
renderUsers();
}
async function refreshStats(options = {}) {
if (options.showLoading) {
state.trafficLoading = true;
renderStats();
}
try {
const data = await api(`/api/stats?range=${encodeURIComponent(state.trafficRange)}`);
state.stats = data;
return data;
} finally {
state.trafficLoading = false;
renderStats();
}
}
async function changeTrafficRange(range) {
const next = trafficRanges.includes(range) ? range : "1h";
if (next === state.trafficRange && state.stats?.range === next) return;
const previous = state.trafficRange;
state.trafficRange = next;
try {
await refreshStats({ showLoading: true });
} catch (err) {
state.trafficRange = previous;
state.trafficLoading = false;
renderStats();
toast(err.message);
}
}
async function addUser(name) {
@@ -992,15 +1043,18 @@ async function setUserEnabled(name, enabled) {
const message = data.enabled ? t("keyEnabled") : t("keyDisabled");
addEvent(message, name);
toast(t("changesApplyInBackground"));
setTimeout(() => refreshAll().catch((err) => toast(err.message)), 1200);
try {
await refreshUsers();
} catch (refreshErr) {
toast(refreshErr.message);
}
setTimeout(() => refreshAll().catch((err) => toast(err.message)), 1400);
} catch (err) {
state.users = previousUsers;
toast(err.message);
} finally {
setTimeout(() => {
state.pendingUsers.delete(name);
renderUsers();
}, 700);
state.pendingUsers.delete(name);
renderUsers();
}
}
@@ -1124,10 +1178,7 @@ document.addEventListener("click", async (eventObj) => {
} else if (button.id === "menuBtn") {
$("#sidebar").classList.toggle("open");
} else if (button.dataset.trafficRange) {
state.trafficRange = trafficRanges.includes(button.dataset.trafficRange) ? button.dataset.trafficRange : "1h";
updateTrafficControls();
renderStats();
refreshStats().catch((err) => toast(err.message));
changeTrafficRange(button.dataset.trafficRange);
} else if (button.dataset.trafficView) {
state.trafficView = button.dataset.trafficView === "table" ? "table" : "chart";
renderStats();

View File

@@ -67,7 +67,10 @@
</div>
<div class="port-map" id="port443Map">
<div class="port-map-head">
<span>443</span>
<div class="port-badge">
<span id="port443Number">443</span>
<small id="port443Configured">public</small>
</div>
<strong id="port443Summary">--</strong>
</div>
<div id="port443List" class="port-list"></div>

View File

@@ -360,7 +360,12 @@ h2 {
gap: 12px;
}
.port-map-head span {
.port-badge {
display: grid;
gap: 4px;
}
.port-badge span {
display: inline-grid;
place-items: center;
width: 54px;
@@ -372,6 +377,13 @@ h2 {
font-size: 20px;
}
.port-badge small {
color: var(--muted);
font-size: 11px;
font-weight: 800;
text-align: center;
}
.port-status {
color: var(--muted);
font-size: 12px;
@@ -1163,6 +1175,7 @@ td small {
}
.actions {
display: flex;
justify-content: stretch;
flex-wrap: wrap;
}