import { sql } from "@/db"; import { PageTitle } from "@/components/admin-ui"; export default async function Analytics() { const [top, searches, referrers, byDay] = await Promise.all([ sql`SELECT p.identifier, p.title, p.view_count::int AS views, p.download_count::int AS downloads, (SELECT count(*)::int FROM events e WHERE e.paper_id = p.id AND e.kind = 'view' AND NOT e.is_bot AND e.day > current_date - 7) AS views_7d FROM papers p WHERE p.status = 'published' ORDER BY p.view_count DESC, p.download_count DESC LIMIT 25`, sql`SELECT query, mode, count(*)::int AS n, avg(results)::int AS avg_results, avg(took_ms)::int AS ms FROM search_log WHERE at > now() - interval '90 days' GROUP BY query, mode ORDER BY n DESC LIMIT 40`, sql`SELECT referrer_host, count(*)::int AS n FROM events WHERE NOT is_bot AND referrer_host IS NOT NULL AND day > current_date - 30 GROUP BY referrer_host ORDER BY n DESC LIMIT 20`, sql`SELECT day::text, count(*) FILTER (WHERE kind = 'view')::int AS views, count(*) FILTER (WHERE kind = 'download')::int AS downloads, count(*) FILTER (WHERE is_bot)::int AS bots FROM events WHERE day > current_date - 30 GROUP BY day ORDER BY day DESC`, ]); return (

Views = distinct daily visitors per paper page (sha256 of IP + user agent + daily salt, not reversible; bots excluded). Downloads = manuscript requests. Raw events are kept 400 days; daily aggregates are kept indefinitely. No cookies, no third-party analytics.

Most viewed papers (all time)

{top.map((p) => )}
IDTitleViews7 dDownloads
{p.identifier}{p.title}{p.views}{p.views_7d}{p.downloads}

Daily events (raw, last 30 days)

{byDay.map((d) => )}
DayViewsDownloadsBot hits
{d.day}{d.views}{d.downloads}{d.bots}

Searches (90 days)

{searches.map((s, i) => )}
QueryModeCountAvg resultsms
{s.query}{s.mode}{s.n}{s.avg_results}{s.ms}

Referrers (30 days)

{referrers.map((r) => )}
HostVisits
{r.referrer_host}{r.n}
); }