From 79826cd26d4f3ba009426c98d1444f34d10eb740 Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Fri, 24 Jul 2026 11:43:43 +0200 Subject: [PATCH] Compact live visitor panel --- public/admin.css | 38 +++++++++++++++++++++++++++----------- public/admin.js | 32 +++++++++++++++++--------------- server.js | 2 ++ 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/public/admin.css b/public/admin.css index dc80caf..c3b3948 100644 --- a/public/admin.css +++ b/public/admin.css @@ -381,16 +381,18 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } overflow: auto; } .visitor-card { - padding: 12px 14px; + padding: 10px 12px; border-bottom: 1px solid var(--line); background: white; } +.visitor-card.clickable { cursor: pointer; } +.visitor-card.clickable:hover { background: #f8fbf9; } .visitor-card-head { display: grid; grid-template-columns: 34px minmax(0, 1fr) 10px; gap: 9px; align-items: center; - margin-bottom: 10px; + margin-bottom: 7px; } .visitor-card-head strong, .visitor-card-head small { @@ -418,21 +420,35 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } background: #2f9e44; box-shadow: 0 0 0 4px rgb(47 158 68 / 14%); } -.visitor-card dl { - display: grid; - grid-template-columns: 68px minmax(0, 1fr); - gap: 5px 8px; - margin: 0; +.visitor-compact-meta { + display: flex; + align-items: center; + gap: 7px; + min-width: 0; + color: var(--muted); font-size: 12px; } -.visitor-card dt { color: var(--muted); } -.visitor-card dd { - min-width: 0; - margin: 0; +.visitor-compact-meta span { + flex: 0 0 auto; + max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.visitor-page, +.visitor-referrer { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +} +.visitor-page { margin-top: 6px; } +.visitor-referrer { + margin-top: 3px; + color: var(--muted); +} .visitor-card a { color: var(--ink); text-decoration: none; } .visitor-card a:hover { color: var(--brand); text-decoration: underline; } .message { diff --git a/public/admin.js b/public/admin.js index ed6b56a..6fa3603 100644 --- a/public/admin.js +++ b/public/admin.js @@ -199,6 +199,14 @@ $("#messages").addEventListener("click", (event) => { openPhotoPreview(preview.dataset.photoUrl, preview.dataset.photoName); }); +$("#visitorList").addEventListener("click", async (event) => { + if (event.target.closest("a")) return; + const card = event.target.closest("[data-visitor-conversation]"); + const conversationId = card?.dataset.visitorConversation; + if (!conversationId) return; + await openConversation(conversationId); +}); + async function showApp() { $("#loginView").classList.add("hidden"); $("#appView").classList.remove("hidden"); @@ -320,7 +328,7 @@ function renderConversations() { function renderVisitors() { $("#visitorCount").textContent = `${state.visitors.length} online`; $("#visitorList").innerHTML = state.visitors.length ? state.visitors.map((visitor) => ` -
+
${escapeHtml(visitor.initials || "??")}
@@ -329,20 +337,14 @@ function renderVisitors() {
-
-
Zeme
-
${visitor.countryFlag || ""} ${escapeHtml(visitor.countryCode || "?")}
-
Stranka
-
${escapeHtml(pageTitle(visitor))}
-
Prisiel z
-
${visitor.referrer ? `${escapeHtml(referrerLabel(visitor.referrer))}` : "primy vstup"}
-
Cas
-
${formatDuration(visitor.sessionSeconds)}
-
Navstevy
-
${Number(visitor.visitCount || visitor.pageCount || 1)}
-
Chaty
-
${Number(visitor.conversationCount || 0)}
-
+
+ ${visitor.countryFlag || escapeHtml(visitor.countryCode || "?")} + ${formatDuration(visitor.sessionSeconds)} + ${Number(visitor.visitCount || visitor.pageCount || 1)} navst. + ${Number(visitor.conversationCount || 0)} chaty +
+ ${escapeHtml(pageTitle(visitor))} +
${visitor.referrer ? escapeHtml(referrerLabel(visitor.referrer)) : "primy vstup"}${visitor.lastConversationId ? "" : " ยท bez chatu"}
`).join("") : `

Zatim nikdo online.

`; } diff --git a/server.js b/server.js index 7a0d3f9..d39a9bc 100644 --- a/server.js +++ b/server.js @@ -625,6 +625,7 @@ function updateVisitorContext(conversationId, body) { function currentVisitors() { const rows = db.prepare(` SELECT vs.*, s.site_key, + (SELECT c.public_id FROM conversations c WHERE c.site_id = vs.site_id AND c.visitor_token = vs.visitor_token ORDER BY datetime(c.last_message_at) DESC, c.id DESC LIMIT 1) AS last_conversation_id, (SELECT COUNT(*) FROM conversations c WHERE c.site_id = vs.site_id AND c.visitor_token = vs.visitor_token) AS visitor_conversation_count FROM visitor_sessions vs JOIN sites s ON s.id = vs.site_id @@ -650,6 +651,7 @@ function formatVisitorSession(row) { pageCount: new Set(history.map((item) => item.url).filter(Boolean)).size, visitCount: Math.max(1, history.length), conversationCount: row.visitor_conversation_count || 0, + lastConversationId: row.last_conversation_id, device, language: row.language, timezone: row.timezone,