From 349f42346e313103d67e652c5991a3a78b7cb8ae Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Thu, 30 Jul 2026 13:43:28 +0200 Subject: [PATCH] Stabilize visitor panel ordering --- public/admin.js | 41 ++++++++++++++++++++++++++++++++++++++--- server.js | 5 +++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/public/admin.js b/public/admin.js index e1078ae..a614072 100644 --- a/public/admin.js +++ b/public/admin.js @@ -24,7 +24,8 @@ const state = { me: null, operatorPresence: {}, operatorTyping: {}, - operatorPresenceTimer: null + operatorPresenceTimer: null, + visitorRenderKey: "" }; const $ = (selector) => document.querySelector(selector); @@ -411,7 +412,7 @@ async function loadConversations() { async function loadVisitors() { const data = await api("/api/admin/visitors"); - state.visitors = data.visitors; + state.visitors = stableVisitors(data.visitors || []); state.visitorsLoaded = true; syncConversationVisitors(); renderConversations(); @@ -474,7 +475,7 @@ function connectEvents() { return; } if (data.type === "visitors:update") { - state.visitors = data.visitors; + state.visitors = stableVisitors(data.visitors || []); state.visitorsLoaded = true; syncConversationVisitors(); renderConversations(); @@ -1068,6 +1069,9 @@ function conversationVisitorName(item) { function renderVisitors() { const visitors = state.hideBots ? state.visitors.filter((visitor) => !visitor.isBot) : state.visitors; const botCount = state.visitors.filter((visitor) => visitor.isBot).length; + const nextRenderKey = visitorListRenderKey(visitors, botCount); + if (nextRenderKey === state.visitorRenderKey) return; + state.visitorRenderKey = nextRenderKey; $("#hideBotsToggle").checked = state.hideBots; $("#visitorCount").textContent = `${visitors.length} na webu${state.hideBots && botCount ? ` ยท ${botCount} bot` : ""}`; $("#visitorList").innerHTML = visitors.length ? visitors.map((visitor) => ` @@ -1092,6 +1096,37 @@ function renderVisitors() { `).join("") : `

Zatim nikdo online.

`; } +function stableVisitors(visitors) { + return [...visitors].sort((a, b) => { + const aFirst = Date.parse(a.firstSeenAt || "") || 0; + const bFirst = Date.parse(b.firstSeenAt || "") || 0; + if (aFirst !== bFirst) return aFirst - bFirst; + return String(a.id || "").localeCompare(String(b.id || "")); + }); +} + +function visitorListRenderKey(visitors, botCount) { + return JSON.stringify({ + hideBots: state.hideBots, + botCount, + visitors: visitors.map((visitor) => ({ + id: visitor.id, + label: visitor.label, + presence: visitor.presence, + currentUrl: visitor.currentUrl, + referrer: visitor.referrer, + lastConversationId: visitor.lastConversationId, + active: visitor.lastConversationId === state.activeId, + isBot: visitor.isBot, + botReason: visitor.botReason, + pageCount: visitor.pageCount, + visitCount: visitor.visitCount, + conversationCount: visitor.conversationCount, + sessionMinute: Math.floor(Number(visitor.sessionSeconds || 0) / 60) + })) + }); +} + function presenceDot(presence) { const value = ["active", "idle", "offline"].includes(presence) ? presence : "offline"; const label = value === "active" ? "Aktivni" : value === "idle" ? "Neaktivni" : "Offline"; diff --git a/server.js b/server.js index 091fea2..d23eb15 100644 --- a/server.js +++ b/server.js @@ -1825,8 +1825,8 @@ function currentVisitors() { FROM visitor_sessions vs JOIN sites s ON s.id = vs.site_id LEFT JOIN visitor_profiles vp ON vp.site_id = vs.site_id AND vp.visitor_token = vs.visitor_token - WHERE datetime(vs.last_seen_at) >= datetime('now', '-${presence.offlineSeconds} seconds') - ORDER BY datetime(vs.last_seen_at) DESC + WHERE datetime(COALESCE(vs.last_activity_at, vs.last_seen_at)) >= datetime('now', '-${presence.offlineSeconds} seconds') + ORDER BY datetime(vs.first_seen_at) ASC, vs.id ASC LIMIT 100 `).all(); return rows.map((row) => formatVisitorSession(row, presence)); @@ -1861,6 +1861,7 @@ function formatVisitorSession(row, thresholds = adminPresenceSettings()) { countryCode, countryFlag: countryCode ? flagEmoji(countryCode) : "", sessionSeconds: sessionSeconds(row.browsing_history_json, row.first_seen_at), + firstSeenAt: row.first_seen_at, lastActivityAt: row.last_activity_at, lastSeenAt: row.last_seen_at };