added log filter
Build and Publish / Build and Publish Docker Image (push) Successful in 1m43s

This commit is contained in:
ab
2026-09-20 20:11:18 +03:00
parent 95154c56e7
commit 866118497f
4 changed files with 61 additions and 3 deletions
+13 -1
View File
@@ -1549,6 +1549,8 @@ async fn analytics_events(
let query_str = request.uri().query().unwrap_or(""); let query_str = request.uri().query().unwrap_or("");
let mut before: Option<i64> = None; let mut before: Option<i64> = None;
let mut limit: usize = 40; let mut limit: usize = 40;
let mut types: Vec<String> = Vec::new();
let mut types_present = false;
for pair in query_str.split('&') { for pair in query_str.split('&') {
if let Some(v) = pair.strip_prefix("before=") { if let Some(v) = pair.strip_prefix("before=") {
before = v.parse().ok(); before = v.parse().ok();
@@ -1556,10 +1558,20 @@ async fn analytics_events(
&& let Ok(n) = v.parse::<usize>() && let Ok(n) = v.parse::<usize>()
{ {
limit = n.clamp(1, 100); 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 { let next_cursor = if page.len() == limit {
page.last().map(|e| e.id.unwrap()) page.last().map(|e| e.id.unwrap())
} else { } else {
+6
View File
@@ -344,14 +344,20 @@ pub async fn media_viewed_recently(
/// The ORM exposes neither `ORDER BY` nor raw row reads, so ordering and slicing /// 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 /// 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. /// 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( pub async fn recent_events(
db: &Database, db: &Database,
before: Option<i64>, before: Option<i64>,
limit: usize, limit: usize,
types: Option<&[String]>,
) -> cot::Result<Vec<AnalyticsEvent>> { ) -> cot::Result<Vec<AnalyticsEvent>> {
// `Auto<i64>` is not `Ord`, so the id cursor can't be pushed into the query; // `Auto<i64>` 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). // filter and sort in Rust instead (acceptable for this site's volume).
let mut events = AnalyticsEvent::objects().all(db).await?; 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())); events.sort_by_key(|e| std::cmp::Reverse(e.id.unwrap()));
Ok(events Ok(events
.into_iter() .into_iter()
+3
View File
@@ -216,6 +216,7 @@ pub struct Translations {
pub analytics_summary_period: &'static str, pub analytics_summary_period: &'static str,
pub analytics_open_dashboard: &'static str, pub analytics_open_dashboard: &'static str,
pub analytics_log_title: &'static str, pub analytics_log_title: &'static str,
pub analytics_log_show: &'static str,
pub analytics_log_empty: &'static str, pub analytics_log_empty: &'static str,
pub analytics_log_end: &'static str, pub analytics_log_end: &'static str,
pub analytics_log_visitor: &'static str, pub analytics_log_visitor: &'static str,
@@ -537,6 +538,7 @@ static RU: Translations = Translations {
analytics_summary_period: "за последние 30 дней", analytics_summary_period: "за последние 30 дней",
analytics_open_dashboard: "Открыть подробную статистику", analytics_open_dashboard: "Открыть подробную статистику",
analytics_log_title: "Последние события", analytics_log_title: "Последние события",
analytics_log_show: "Показывать",
analytics_log_empty: "Событий пока нет.", analytics_log_empty: "Событий пока нет.",
analytics_log_end: "Это все события.", analytics_log_end: "Это все события.",
analytics_log_visitor: "Посетитель", analytics_log_visitor: "Посетитель",
@@ -848,6 +850,7 @@ static EN: Translations = Translations {
analytics_summary_period: "over the last 30 days", analytics_summary_period: "over the last 30 days",
analytics_open_dashboard: "Open detailed analytics", analytics_open_dashboard: "Open detailed analytics",
analytics_log_title: "Recent events", analytics_log_title: "Recent events",
analytics_log_show: "Show",
analytics_log_empty: "No events yet.", analytics_log_empty: "No events yet.",
analytics_log_end: "That's all events.", analytics_log_end: "That's all events.",
analytics_log_visitor: "Visitor", analytics_log_visitor: "Visitor",
+39 -2
View File
@@ -84,7 +84,15 @@
</div> </div>
<div class="analytics-log-card"> <div class="analytics-log-card">
<h2>{{ t.analytics_log_title }}</h2> <div class="analytics-log-head">
<h2>{{ t.analytics_log_title }}</h2>
<div class="analytics-metric-toggles analytics-log-filters">
<span class="toggles-label">{{ t.analytics_log_show }}:</span>
<label><input type="checkbox" class="log-type-toggle" data-type="landing_view"> {{ t.analytics_ev_landing_view }}</label>
<label><input type="checkbox" class="log-type-toggle" data-type="portal_open" checked> {{ t.analytics_ev_portal_open }}</label>
<label><input type="checkbox" class="log-type-toggle" data-type="media_view" checked> {{ t.analytics_ev_media_view }}</label>
</div>
</div>
<div class="analytics-log" id="anLog"></div> <div class="analytics-log" id="anLog"></div>
</div> </div>
@@ -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; } .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 { 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; } .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 { 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; } .an-log-row:last-child { border-bottom:none; }
@@ -496,13 +506,36 @@
return row; 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() { function loadLog() {
if (logLoading || logDone) { return; } if (logLoading || logDone) { return; }
var types = selectedLogTypes();
if (types.length === 0) {
logEl.replaceChildren(logStatus(L.log_empty));
logDone = true;
return;
}
logLoading = true; logLoading = true;
var busy = logStatus(L.loading); var busy = logStatus(L.loading);
logEl.appendChild(busy); logEl.appendChild(busy);
var params = new URLSearchParams({ limit: '40' }); var params = new URLSearchParams({ limit: '40' });
if (logCursor) { params.set('before', logCursor); } if (logCursor) { params.set('before', logCursor); }
types.forEach(function (t) { params.append('types', t); });
fetch('/admin/analytics/events?' + params.toString(), { credentials: 'same-origin' }) fetch('/admin/analytics/events?' + params.toString(), { credentials: 'same-origin' })
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (data) { .then(function (data) {
@@ -534,6 +567,10 @@
if (logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 40) { loadLog(); } if (logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 40) { loadLog(); }
}); });
document.querySelectorAll('.log-type-toggle').forEach(function (cb) {
cb.addEventListener('change', resetLog);
});
initChart(); initChart();
setRange(30); setRange(30);
load(); load();