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)
| ID | Title | Views | 7 d | Downloads |
{top.map((p) => | {p.identifier} | {p.title} | {p.views} | {p.views_7d} | {p.downloads} |
)}
Daily events (raw, last 30 days)
| Day | Views | Downloads | Bot hits |
{byDay.map((d) => | {d.day} | {d.views} | {d.downloads} | {d.bots} |
)}
Searches (90 days)
| Query | Mode | Count | Avg results | ms |
{searches.map((s, i) => | {s.query} | {s.mode} | {s.n} | {s.avg_results} | {s.ms} |
)}
Referrers (30 days)
| Host | Visits |
{referrers.map((r) => | {r.referrer_host} | {r.n} |
)}
);
}