import { all, first } from "./db"; import { allSites } from "./sites/store"; import { siteDirectoryReady, siteDirectoryStats, type SiteStats } from "./sites/directory"; const DAY_MS = 24 * 60 * 60 * 1000; type Count = { count: number }; async function count(sql: string, ...params: (string | number)[]): Promise { return (await first(sql, ...params))?.count ?? 0; } export type AdminOrgRow = { id: string; name: string; members: number; repos: number; sites: number; reports: number }; export type AdminUserRow = { id: string; name: string; email: string; verified: boolean; isAdmin: boolean; repos: number; reports: number; createdAt: number; }; /** Until the directory is backfilled: every site row, read from its scope. */ async function scannedSiteStats(now: number, week: number): Promise { const allSiteRows = await allSites(); const siteCount = (keep: (site: (typeof allSiteRows)[number]) => boolean) => allSiteRows.filter(keep).length; const siteOwner = new Map(allSiteRows.map((site) => [site.id, site.owner_id])); const sitesByOwner = new Map(); for (const site of allSiteRows) { if (site.owner_id) sitesByOwner.set(site.owner_id, (sitesByOwner.get(site.owner_id) ?? 0) + 1); } const openSiteReports = await all<{ site_id: string }>( "SELECT site_id FROM abuse_reports WHERE status = 'open' AND site_id IS NOT NULL", ); const reportsByOwner = new Map(); for (const report of openSiteReports) { const owner = siteOwner.get(report.site_id); if (owner) reportsByOwner.set(owner, (reportsByOwner.get(owner) ?? 0) + 1); } return { totals: { total: allSiteRows.length, active: siteCount((site) => site.disabled_reason === null && site.active_deployment_id !== null), paused: siteCount((site) => site.paused_at !== null), disabled: siteCount((site) => site.disabled_reason !== null && site.paused_at === null), scratch: siteCount((site) => site.scratch === 1 && (site.expires_at === null || site.expires_at > now)), autoPausedWeek: siteCount((site) => site.paused_at !== null && site.paused_at > week), }, sitesByOwner, reportsByOwner, }; } /** * Site-wide counters for the admin dashboard: indexed aggregates over the D1 directory, sites included (their * rows live in RepoState; `site_directory` keeps a copy of what these counts need). */ export async function adminOverview() { const now = Date.now(); const week = now - 7 * DAY_MS; const month = now - 30 * DAY_MS; const { totals: siteTotals, sitesByOwner, reportsByOwner, } = (await siteDirectoryReady()) ? await siteDirectoryStats(now, week) : await scannedSiteStats(now, week); const [ users, verifiedUsers, newUsersWeek, newUsersMonth, admins, twoFactorUsers, orgs, newOrgsMonth, repos, publicRepos, openReports, reportsWeek, ] = await Promise.all([ count("SELECT COUNT(*) AS count FROM users"), count("SELECT COUNT(*) AS count FROM users WHERE email_verified_at IS NOT NULL"), count("SELECT COUNT(*) AS count FROM accounts WHERE kind = 'user' AND created_at > ?", week), count("SELECT COUNT(*) AS count FROM accounts WHERE kind = 'user' AND created_at > ?", month), count("SELECT COUNT(*) AS count FROM users WHERE is_admin = 1"), count("SELECT COUNT(*) AS count FROM users WHERE totp_enabled_at IS NOT NULL"), count("SELECT COUNT(*) AS count FROM accounts WHERE kind = 'org'"), count("SELECT COUNT(*) AS count FROM accounts WHERE kind = 'org' AND created_at > ?", month), count("SELECT COUNT(*) AS count FROM repos"), count("SELECT COUNT(*) AS count FROM repos WHERE visibility = 'public'"), count("SELECT COUNT(*) AS count FROM abuse_reports WHERE status = 'open'"), count("SELECT COUNT(*) AS count FROM abuse_reports WHERE created_at > ?", week), ]); const [orgRows, recentUsers, reportsByCategory] = await Promise.all([ all>( `SELECT a.id, a.name, (SELECT COUNT(*) FROM org_members m WHERE m.org_id = a.id) AS members, (SELECT COUNT(*) FROM repos r WHERE r.owner_id = a.id) AS repos FROM accounts a WHERE a.kind = 'org'`, ), all<{ id: string; name: string; email: string; email_verified_at: number | null; is_admin: number; repos: number; reports: number; created_at: number; }>( `SELECT a.id, a.name, u.email, u.email_verified_at, u.is_admin, a.created_at, (SELECT COUNT(*) FROM repos r WHERE r.owner_id = a.id) AS repos, 0 AS reports FROM accounts a JOIN users u ON u.account_id = a.id ORDER BY a.created_at DESC LIMIT 25`, ), all<{ category: string; count: number }>( "SELECT category, COUNT(*) AS count FROM abuse_reports WHERE created_at > ? GROUP BY category ORDER BY count DESC", month, ), ]); const topOrgs: AdminOrgRow[] = orgRows .map((org) => ({ ...org, sites: sitesByOwner.get(org.id) ?? 0, reports: reportsByOwner.get(org.id) ?? 0 })) .sort((left, right) => right.reports - left.reports || right.members - left.members || right.repos - left.repos) .slice(0, 25); return { users: { total: users, verified: verifiedUsers, newWeek: newUsersWeek, newMonth: newUsersMonth, admins }, security: { twoFactorUsers }, orgs: { total: orgs, newMonth: newOrgsMonth }, repos: { total: repos, public: publicRepos, private: repos - publicRepos }, sites: { total: siteTotals.total, active: siteTotals.active, paused: siteTotals.paused, disabled: siteTotals.disabled, scratch: siteTotals.scratch, }, abuse: { open: openReports, week: reportsWeek, autoPausedWeek: siteTotals.autoPausedWeek, byCategory: reportsByCategory, }, topOrgs, recentUsers: recentUsers.map((row): AdminUserRow => ({ id: row.id, name: row.name, email: row.email, verified: row.email_verified_at !== null, isAdmin: row.is_admin === 1, repos: row.repos, reports: reportsByOwner.get(row.id) ?? 0, createdAt: row.created_at, })), }; }