131 lines · 5.5 KB
import { all, first } from "./db";
import { allSites } from "./sites/store";

const DAY_MS = 24 * 60 * 60 * 1000;

type Count = { count: number };

async function count(sql: string, ...params: (string | number)[]): Promise<number> {
  return (await first<Count>(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;
};

/**
 * Site-wide counters for the admin dashboard: indexed aggregates over the D1 directory, plus the sites, which
 * live in their repositories' objects and are gathered with one fan-out (the list is small).
 */
export async function adminOverview() {
  const now = Date.now();
  const week = now - 7 * DAY_MS;
  const month = now - 30 * DAY_MS;
  const allSiteRows = await allSites();
  const siteCount = (keep: (site: (typeof allSiteRows)[number]) => boolean) => allSiteRows.filter(keep).length;
  const sites = allSiteRows.length;
  const activeSites = siteCount((site) => site.disabled_reason === null && site.active_deployment_id !== null);
  const pausedSites = siteCount((site) => site.paused_at !== null);
  const disabledSites = siteCount((site) => site.disabled_reason !== null && site.paused_at === null);
  const scratchSites = siteCount((site) => site.scratch === 1 && (site.expires_at === null || site.expires_at > now));
  const autoPausedWeek = siteCount((site) => site.paused_at !== null && site.paused_at > week);
  const siteOwner = new Map(allSiteRows.map((site) => [site.id, site.owner_id]));