use askama::Template;
use askama_web::WebTemplate;
use axum::{extract::State, response::IntoResponse};
use std::time::Duration;
use crate::{
storage::query_log::{QueryLogCounts, QueryLogRepository},
telemetry::StatsSnapshot,
time::{self, Clock},
web::{AppState, Chrome, auth::CurrentUser, render::DomainDisplay},
};
const TOP_N: usize = 10;
const WINDOW: Duration = time::days(1);
impl AppState {
pub async fn dashboard(user: CurrentUser, State(state): State<AppState>) -> impl IntoResponse {
let snapshot = state.telemetry.stats.snapshot(TOP_N);
let blocklist_size = state.resolver.blocklist().len();
let repo = state.db.query_log();
let since = Clock::millis_ago(WINDOW);
let window = WindowStats {
counts: repo.counts_since(since).await.unwrap_or_default(),
top_domains: repo
.top_domains_since(since, TOP_N as i64)
.await
.unwrap_or_default(),
top_clients: repo
.top_clients_since(since, TOP_N as i64)
.await
.unwrap_or_default(),
};
DashboardTemplate::new(
state.chrome("dashboard", &user).await,
snapshot,
blocklist_size,
window,
)
}
}
struct WindowStats {
counts: QueryLogCounts,
top_domains: Vec<(String, i64)>,
top_clients: Vec<(String, i64)>,
}
#[derive(Template, WebTemplate)]
#[template(path = "dashboard.html")]
struct DashboardTemplate {
chrome: Chrome,
total: u64,
blocked: u64,
cached: u64,
forwarded: u64,
blocklist_size: String,
top_domains: Vec<(String, String)>,
top_clients: Vec<(String, String)>,
window_total: String,
window_blocked: String,
window_cached: String,
window_forwarded: String,
window_top_domains: Vec<(String, String)>,
window_top_clients: Vec<(String, String)>,
}
impl DashboardTemplate {
fn new(
chrome: Chrome,
snap: StatsSnapshot,
blocklist_size: usize,
window: WindowStats,
) -> Self {
Self {
chrome,
total: snap.total,
blocked: snap.blocked,
cached: snap.cached,
forwarded: snap.forwarded,
blocklist_size: group(blocklist_size as u64),
top_domains: snap
.top_domains
.into_iter()
.map(|(d, c)| (d.display_domain().to_owned(), group(c)))
.collect(),
top_clients: snap
.top_clients
.into_iter()
.map(|(ip, c)| (ip.to_string(), group(c)))
.collect(),
window_total: group(window.counts.total.max(0) as u64),
window_blocked: group(window.counts.blocked.max(0) as u64),
window_cached: group(window.counts.cached.max(0) as u64),
window_forwarded: group(window.counts.forwarded.max(0) as u64),
window_top_domains: window
.top_domains
.into_iter()
.map(|(d, c)| (d.display_domain().to_owned(), group(c.max(0) as u64)))
.collect(),
window_top_clients: window
.top_clients
.into_iter()
.map(|(ip, c)| (ip, group(c.max(0) as u64)))
.collect(),
}
}
}
pub(crate) fn group(n: u64) -> String {
let digits = n.to_string();
let len = digits.len();
let mut out = String::with_capacity(len + len / 3);
for (i, ch) in digits.chars().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
out.push(',');
}
out.push(ch);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn group_inserts_thousands_separators() {
assert_eq!(group(0), "0");
assert_eq!(group(42), "42");
assert_eq!(group(1_000), "1,000");
assert_eq!(group(12_403), "12,403");
assert_eq!(group(1_234_567), "1,234,567");
}
fn test_chrome() -> Chrome {
Chrome {
theme: "auto".to_owned(),
active: "dashboard",
show_nav: true,
authenticated: true,
csrf_token: "tok".to_owned(),
}
}
#[test]
fn template_seeds_signals_and_tables() {
let snap = StatsSnapshot {
total: 1000,
blocked: 382,
cached: 100,
forwarded: 518,
blocked_ratio: 0.382,
top_domains: vec![("ads.example.com.".to_owned(), 50)],
top_clients: vec![("192.168.1.10".parse().unwrap(), 120)],
};
let window = WindowStats {
counts: QueryLogCounts {
total: 2400,
blocked: 900,
cached: 500,
forwarded: 1000,
},
top_domains: vec![("win.example.com.".to_owned(), 77)],
top_clients: vec![("10.9.8.7".to_owned(), 64)],
};
let html = DashboardTemplate::new(test_chrome(), snap, 65432, window)
.render()
.expect("render");
assert!(html.contains("queries: 1000"));
assert!(html.contains("blocked: 382"));
assert!(html.contains("65,432"));
assert!(html.contains("ads.example.com"));
assert!(!html.contains("ads.example.com."));
assert!(html.contains("192.168.1.10"));
assert!(html.contains("Last 24 hours (persisted)"));
assert!(html.contains("2,400"));
assert!(html.contains("win.example.com"));
assert!(!html.contains("win.example.com."));
assert!(html.contains("10.9.8.7"));
}
#[test]
fn template_empty_window_renders_zeros_without_panic() {
let snap = StatsSnapshot {
total: 0,
blocked: 0,
cached: 0,
forwarded: 0,
blocked_ratio: 0.0,
top_domains: vec![],
top_clients: vec![],
};
let window = WindowStats {
counts: QueryLogCounts::default(),
top_domains: vec![],
top_clients: vec![],
};
let html = DashboardTemplate::new(test_chrome(), snap, 0, window)
.render()
.expect("render");
assert!(html.contains("No queries in the last 24 hours."));
}
}