diff --git a/docker-compose.yml b/docker-compose.yml index 198df198..53784309 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,4 +13,4 @@ services: XUI_ENABLE_FAIL2BAN: "true" tty: true network_mode: host - restart: unless-stopped + restart: unless-stopped \ No newline at end of file diff --git a/install.sh b/install.sh index af6b8a51..2ee1fd2c 100644 --- a/install.sh +++ b/install.sh @@ -581,7 +581,7 @@ prompt_and_setup_ssl() { # 3.1 Request Domain to compose Panel URL later read -rp "Please enter domain name certificate issued for: " custom_domain - custom_domain="${custom_domain// /}" # Убираем пробелы + custom_domain="${custom_domain// /}" # Remove spaces # 3.2 Loop for Certificate Path while true; do diff --git a/update.sh b/update.sh index b9cb3ddc..fd7cfc3c 100755 --- a/update.sh +++ b/update.sh @@ -609,7 +609,7 @@ prompt_and_setup_ssl() { # 3.1 Request Domain to compose Panel URL later read -rp "Please enter domain name certificate issued for: " custom_domain - custom_domain="${custom_domain// /}" # Убираем пробелы + custom_domain="${custom_domain// /}" # Remove spaces # 3.2 Loop for Certificate Path while true; do diff --git a/web/assets/js/model/dbinbound.js b/web/assets/js/model/dbinbound.js index befc618e..9132e94e 100644 --- a/web/assets/js/model/dbinbound.js +++ b/web/assets/js/model/dbinbound.js @@ -91,6 +91,10 @@ class DBInbound { } toInbound() { + if (this._cachedInbound) { + return this._cachedInbound; + } + let settings = {}; if (!ObjectUtil.isEmpty(this.settings)) { settings = JSON.parse(this.settings); @@ -116,7 +120,21 @@ class DBInbound { sniffing: sniffing, clientStats: this.clientStats, }; - return Inbound.fromJson(config); + + this._cachedInbound = Inbound.fromJson(config); + return this._cachedInbound; + } + + getClientStats(email) { + if (!this._clientStatsMap) { + this._clientStatsMap = new Map(); + if (this.clientStats && Array.isArray(this.clientStats)) { + for (const stats of this.clientStats) { + this._clientStatsMap.set(stats.email, stats); + } + } + } + return this._clientStatsMap.get(email); } isMultiUser() { diff --git a/web/assets/js/websocket.js b/web/assets/js/websocket.js index ccafef87..1009aaf7 100644 --- a/web/assets/js/websocket.js +++ b/web/assets/js/websocket.js @@ -43,7 +43,7 @@ class WebSocketClient { this.ws.onmessage = (event) => { try { // Validate message size (prevent memory issues) - const maxMessageSize = 10 * 1024 * 1024; // 10MB + const maxMessageSize = 100 * 1024 * 1024; // 100MB if (event.data && event.data.length > maxMessageSize) { console.error('WebSocket message too large:', event.data.length, 'bytes'); this.ws.close(); diff --git a/web/controller/websocket.go b/web/controller/websocket.go index 0ad5c845..61e6f879 100644 --- a/web/controller/websocket.go +++ b/web/controller/websocket.go @@ -30,8 +30,10 @@ const ( ) var upgrader = ws.Upgrader{ - ReadBufferSize: 4096, // Increased from 1024 for better performance - WriteBufferSize: 4096, // Increased from 1024 for better performance + ReadBufferSize: 32768, // Huge buffers for huge databases + WriteBufferSize: 32768, // Huge buffers to reduce TCP fragmentation + EnableCompression: true, // Automatically GZIP large messages unconditionally + CheckOrigin: func(r *http.Request) bool { // Check origin for security origin := r.Header.Get("Origin") diff --git a/web/html/form/stream/stream_xhttp.html b/web/html/form/stream/stream_xhttp.html index 447612c9..b43def75 100644 --- a/web/html/form/stream/stream_xhttp.html +++ b/web/html/form/stream/stream_xhttp.html @@ -70,6 +70,8 @@ queryInHeader header + cookie + query @@ -127,7 +129,7 @@ Default (body) body header - query + cookie - + @@ -14,10 +14,7 @@ - - - +
@@ -1304,7 +1301,6 @@ if (!clients || !Array.isArray(clients)) return; index = this.findIndexOfClient(dbInbound.protocol, clients, client); if (index < 0 || !clients[index]) return; - clients[index].enable = !clients[index].enable; clientId = this.getClientId(dbInbound.protocol, clients[index]); await this.updateClient(clients[index], dbInboundId, clientId); this.loading(false); @@ -1317,7 +1313,7 @@ }, getInboundClients(dbInbound) { if (!dbInbound) return null; - const inbound = dbInbound.toInbound(); + const inbound = this.inbounds.find(ib => ib.id === dbInbound.id) || dbInbound.toInbound(); return inbound && inbound.clients ? inbound.clients : null; }, resetClientTraffic(client, dbInboundId, confirmation = true) { @@ -1367,42 +1363,54 @@ isExpiry(dbInbound, index) { return dbInbound.toInbound().isExpiry(index); }, + getClientStats(dbInbound, email) { + if (!dbInbound) return null; + if (!dbInbound._clientStatsMap) { + dbInbound._clientStatsMap = new Map(); + if (dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) { + for (const stats of dbInbound.clientStats) { + dbInbound._clientStatsMap.set(stats.email, stats); + } + } + } + return dbInbound._clientStatsMap.get(email); + }, getUpStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.up : 0; }, getDownStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.down : 0; }, getSumStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.up + clientStats.down : 0; }, getAllTimeClient(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; return clientStats.allTime || (clientStats.up + clientStats.down); }, getRemStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; - remained = clientStats.total - (clientStats.up + clientStats.down); + let remained = clientStats.total - (clientStats.up + clientStats.down); return remained > 0 ? remained : 0; }, clientStatsColor(dbInbound, email) { - if (email.length == 0) return ColorUtils.clientUsageColor(); - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return ColorUtils.clientUsageColor(); + let clientStats = this.getClientStats(dbInbound, email); return ColorUtils.clientUsageColor(clientStats, app.trafficDiff) }, statsProgress(dbInbound, email) { - if (email.length == 0) return 100; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 100; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; if (clientStats.total == 0) return 100; return 100 * (clientStats.down + clientStats.up) / clientStats.total; @@ -1415,11 +1423,11 @@ return 100 * (1 - (remainedSeconds / resetSeconds)); }, statsExpColor(dbInbound, email) { - if (email.length == 0) return '#7a316f'; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return '#7a316f'; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return '#7a316f'; - statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); - expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime); + let statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); + let expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime); switch (true) { case statsColor == "red" || expColor == "red": return "#cf3c3c"; // Red @@ -1432,12 +1440,12 @@ } }, isClientEnabled(dbInbound, email) { - clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null; + let clientStats = dbInbound ? this.getClientStats(dbInbound, email) : null; return clientStats ? clientStats['enable'] : true; }, isClientDepleted(dbInbound, email) { - if (!email || !dbInbound || !dbInbound.clientStats) return false; - const stats = dbInbound.clientStats.find(s => s.email === email); + if (!email || !dbInbound) return false; + const stats = this.getClientStats(dbInbound, email); if (!stats) return false; const total = stats.total ?? 0; const used = (stats.up ?? 0) + (stats.down ?? 0); @@ -1557,12 +1565,18 @@ pagination(obj) { if (this.pageSize > 0 && obj.length > this.pageSize) { // Set page options based on object size - sizeOptions = []; - for (i = this.pageSize; i <= obj.length; i = i + this.pageSize) { - sizeOptions.push(i.toString()); + let sizeOptions = [this.pageSize.toString()]; + const increments = [2, 5, 10, 20]; + for (const m of increments) { + const val = this.pageSize * m; + if (val < obj.length && val <= 1000) { + sizeOptions.push(val.toString()); + } } // Add option to see all in one page - sizeOptions.push(i.toString()); + if (!sizeOptions.includes(obj.length.toString())) { + sizeOptions.push(obj.length.toString()); + } p = { showSizeChanger: true, @@ -1605,11 +1619,25 @@ } }); + // Listen for invalidate signals (sent when payload is too large for WebSocket) + // The server sends a lightweight notification and we re-fetch via REST API + let invalidateTimer = null; + window.wsClient.on('invalidate', (payload) => { + if (payload && (payload.type === 'inbounds' || payload.type === 'traffic')) { + // Debounce to avoid flooding the REST API with multiple invalidate signals + if (invalidateTimer) clearTimeout(invalidateTimer); + invalidateTimer = setTimeout(() => { + invalidateTimer = null; + this.getDBInbounds(); + }, 1000); + } + }); + // Listen for traffic updates window.wsClient.on('traffic', (payload) => { // Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event // because clientTraffics contains delta/incremental values, not total accumulated values. - // Total traffic is updated via the 'inbounds' event which contains accumulated values from database. + // Total traffic is updated via the 'inbounds' WebSocket event (or 'invalidate' fallback for large panels). // Update online clients list in real-time if (payload && Array.isArray(payload.onlineClients)) { @@ -1627,22 +1655,27 @@ this.onlineClients = nextOnlineClients; if (onlineChanged) { // Recalculate client counts to update online status + // Use $set for Vue 2 reactivity — direct array index assignment is not reactive this.dbInbounds.forEach(dbInbound => { const inbound = this.inbounds.find(ib => ib.id === dbInbound.id); if (inbound && this.clientCount[dbInbound.id]) { - this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound); + this.$set(this.clientCount, dbInbound.id, this.getClientCounts(dbInbound, inbound)); } }); + // Always trigger UI refresh — not just when filter is enabled if (this.enableFilter) { this.filterInbounds(); + } else { + this.searchInbounds(this.searchKey); } } } // Update last online map in real-time + // Replace entirely (server sends the full map) to avoid unbounded growth from deleted clients if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') { - this.lastOnlineMap = { ...this.lastOnlineMap, ...payload.lastOnlineMap }; + this.lastOnlineMap = payload.lastOnlineMap; } }); @@ -1697,4 +1730,18 @@ }, }); + {{ template "page/body_end" .}} \ No newline at end of file diff --git a/web/html/index.html b/web/html/index.html index bbbbb708..47645f7d 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -6,7 +6,7 @@ - + @@ -15,9 +15,7 @@