use axum::extract::State; use axum::response::Html; use std::sync::atomic::Ordering; use crate::{db, AppState}; use super::{page, err_page, fmt_ts_ago, short_pubkey, esc}; pub async fn handler(State(state): State) -> Html { match render(&state).await { Ok(h) => h, Err(e) => err_page(&e), } } async fn render(state: &AppState) -> anyhow::Result> { let stats = db::ui_stats(&state.pool).await?; let relays = db::list_relays_ui(&state.pool).await.unwrap_or_default(); let recent = db::recent_torrents(&state.pool, 12).await.unwrap_or_default(); // Stats row let last_seen = stats .last_event_at .map(|ts| fmt_ts_ago(ts)) .unwrap_or_else(|| "never".into()); let stats_html = format!( r#"
{}
Indexed
{}
Publishers
{}
Queued
{}
Published
{last_seen}
Last ingest
"#, stats.torrent_count, stats.publisher_count, stats.queued_count, stats.published_count, ); // Network status let network_html = build_network_html(state); // Relay status let relay_count = state.relay_count.load(Ordering::Relaxed); let relay_rows: String = if relays.is_empty() { "
No relays configured
".into() } else { relays .iter() .map(|r| { let (dot, ts_str) = match r.last_event { Some(ts) => ("dot-ok", format!("last event {}", fmt_ts_ago(ts))), // "no events yet" is normal on a fresh start — the relay is connected // but kindexr hasn't received any NIP-35 events from it this session. None => ("dot-dim", "connected — no events received yet".into()), }; format!( r#"
{url} {ts_str}
"#, url = esc(&r.url), ) }) .collect() }; let relays_html = format!( r#"
Relays — {relay_count} connected / {} configured
{relay_rows}
"#, relays.len(), ); // Recent ingest table let recent_rows: String = if recent.is_empty() { "Nothing indexed yet".into() } else { recent .iter() .map(|r| { let pub_display = r.publisher_name.as_deref() .or(r.publisher_nip05.as_deref()) .map(esc) .unwrap_or_else(|| short_pubkey(&r.pubkey)); format!( r#" {title} {cat} {pub_display} {ts} "#, title = esc(&r.title), cat = esc(&r.category), pk_full = esc(&r.pubkey), ts = fmt_ts_ago(r.ingested_at), ) }) .collect() }; let recent_html = format!( r#"
Recent ingest
{recent_rows}
TitleCategoryPublisherWhen
"# ); let body = format!("

Dashboard

{stats_html}{network_html}{relays_html}{recent_html}"); Ok(page("dashboard", "Dashboard", &body)) } fn build_network_html(state: &AppState) -> String { let proxy = &state.cfg.network.proxy; let listen = &state.cfg.server.listen; // Proxy mode badge let (proxy_badge_class, proxy_label, proxy_detail) = match proxy.mode.as_str() { "tor" => ( "badge-warn", "Tor", "SOCKS5 → 127.0.0.1:9050 — all outbound traffic routed through Tor".to_string(), ), "i2p" => ( "badge-accent", "I2P", "SOCKS5 → 127.0.0.1:4446 — all outbound traffic routed through I2P".to_string(), ), "socks5" => { let addr = proxy.url.trim_start_matches("socks5://").trim_start_matches("socks5h://"); ( "badge-accent", "SOCKS5", format!("SOCKS5 → {addr} — all outbound traffic proxied"), ) } _ => ( "badge-dim", "Direct", format!("No proxy — outbound connections use your host's network stack directly"), ), }; // IP version from listen address let ip_ver = if listen.starts_with('[') || listen.contains("::") { r#"IPv6"# } else if listen.starts_with("0.0.0.0") || listen.starts_with("127.") || listen.chars().next().map_or(false, |c| c.is_ascii_digit()) { r#"IPv4"# } else { "" }; format!( r#"
Network
Proxy / anonymity
{proxy_label}
{proxy_detail}
Listening on
{listen} {ip_ver}
"#, listen = esc(listen), ) }