From 866118497f0c50d6a5e73bc05175c160fcb3afde Mon Sep 17 00:00:00 2001 From: AB Date: Sun, 20 Sep 2026 20:11:18 +0300 Subject: [PATCH] added log filter --- src/admin.rs | 14 +++++++++++- src/analytics.rs | 6 +++++ src/i18n.rs | 3 +++ templates/admin/analytics.html | 41 ++++++++++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/admin.rs b/src/admin.rs index 59a2647..7f15fe2 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1549,6 +1549,8 @@ async fn analytics_events( let query_str = request.uri().query().unwrap_or(""); let mut before: Option = None; let mut limit: usize = 40; + let mut types: Vec = Vec::new(); + let mut types_present = false; for pair in query_str.split('&') { if let Some(v) = pair.strip_prefix("before=") { before = v.parse().ok(); @@ -1556,10 +1558,20 @@ async fn analytics_events( && let Ok(n) = v.parse::() { limit = n.clamp(1, 100); + } else if let Some(v) = pair.strip_prefix("types=") { + types_present = true; + if !v.is_empty() { + types.push(v.to_string()); + } } } - let page = crate::analytics::recent_events(&db, before, limit).await?; + let allowed = if types_present { + Some(types.as_slice()) + } else { + None + }; + let page = crate::analytics::recent_events(&db, before, limit, allowed).await?; let next_cursor = if page.len() == limit { page.last().map(|e| e.id.unwrap()) } else { diff --git a/src/analytics.rs b/src/analytics.rs index 0711455..c105e0b 100644 --- a/src/analytics.rs +++ b/src/analytics.rs @@ -344,14 +344,20 @@ pub async fn media_viewed_recently( /// The ORM exposes neither `ORDER BY` nor raw row reads, so ordering and slicing /// happen in Rust. This is fine for the expected event volume of this site; if /// the table ever grows very large, add a descending index and raw SQL here. +/// `types`: `None` returns all event types; `Some(list)` returns only those +/// (an empty list therefore returns nothing). pub async fn recent_events( db: &Database, before: Option, limit: usize, + types: Option<&[String]>, ) -> cot::Result> { // `Auto` is not `Ord`, so the id cursor can't be pushed into the query; // filter and sort in Rust instead (acceptable for this site's volume). let mut events = AnalyticsEvent::objects().all(db).await?; + if let Some(types) = types { + events.retain(|e| types.iter().any(|t| t == &e.event_type)); + } events.sort_by_key(|e| std::cmp::Reverse(e.id.unwrap())); Ok(events .into_iter() diff --git a/src/i18n.rs b/src/i18n.rs index 042dcd1..61f06c5 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -216,6 +216,7 @@ pub struct Translations { pub analytics_summary_period: &'static str, pub analytics_open_dashboard: &'static str, pub analytics_log_title: &'static str, + pub analytics_log_show: &'static str, pub analytics_log_empty: &'static str, pub analytics_log_end: &'static str, pub analytics_log_visitor: &'static str, @@ -537,6 +538,7 @@ static RU: Translations = Translations { analytics_summary_period: "за последние 30 дней", analytics_open_dashboard: "Открыть подробную статистику", analytics_log_title: "Последние события", + analytics_log_show: "Показывать", analytics_log_empty: "Событий пока нет.", analytics_log_end: "Это все события.", analytics_log_visitor: "Посетитель", @@ -848,6 +850,7 @@ static EN: Translations = Translations { analytics_summary_period: "over the last 30 days", analytics_open_dashboard: "Open detailed analytics", analytics_log_title: "Recent events", + analytics_log_show: "Show", analytics_log_empty: "No events yet.", analytics_log_end: "That's all events.", analytics_log_visitor: "Visitor", diff --git a/templates/admin/analytics.html b/templates/admin/analytics.html index d02e626..af66c28 100644 --- a/templates/admin/analytics.html +++ b/templates/admin/analytics.html @@ -84,7 +84,15 @@
-

{{ t.analytics_log_title }}

+
+

{{ t.analytics_log_title }}

+
+ {{ t.analytics_log_show }}: + + + +
+
@@ -117,7 +125,9 @@ .an-media-missing { width:40px; height:40px; border-radius:6px; background:#f0f0f3; display:inline-flex; align-items:center; justify-content:center; color:#bbb; flex:0 0 auto; } .analytics-log-card { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin-top:20px; } -.analytics-log-card h2 { font-size:1rem; margin:0 0 10px; } +.analytics-log-card h2 { font-size:1rem; margin:0; } +.analytics-log-head { display:flex; flex-wrap:wrap; gap:10px 18px; align-items:center; justify-content:space-between; margin-bottom:10px; } +.analytics-log-filters { margin-bottom:0; } .analytics-log { max-height:460px; overflow-y:auto; border:1px solid #f0f0f0; border-radius:8px; } .an-log-row { display:flex; gap:12px; align-items:flex-start; padding:10px 12px; border-bottom:1px solid #f5f5f5; font-size:.85rem; } .an-log-row:last-child { border-bottom:none; } @@ -496,13 +506,36 @@ return row; } + function selectedLogTypes() { + var types = []; + document.querySelectorAll('.log-type-toggle').forEach(function (cb) { + if (cb.checked) { types.push(cb.getAttribute('data-type')); } + }); + return types; + } + + function resetLog() { + logEl.replaceChildren(); + logCursor = null; + logLoading = false; + logDone = false; + loadLog(); + } + function loadLog() { if (logLoading || logDone) { return; } + var types = selectedLogTypes(); + if (types.length === 0) { + logEl.replaceChildren(logStatus(L.log_empty)); + logDone = true; + return; + } logLoading = true; var busy = logStatus(L.loading); logEl.appendChild(busy); var params = new URLSearchParams({ limit: '40' }); if (logCursor) { params.set('before', logCursor); } + types.forEach(function (t) { params.append('types', t); }); fetch('/admin/analytics/events?' + params.toString(), { credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (data) { @@ -534,6 +567,10 @@ if (logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 40) { loadLog(); } }); + document.querySelectorAll('.log-type-toggle').forEach(function (cb) { + cb.addEventListener('change', resetLog); + }); + initChart(); setRange(30); load();