admin-overview.ts91 lines · main
| 1 | import { isNull, sql } from 'drizzle-orm'; |
| 2 | |
| 3 | import { getDb } from '../db/client.js'; |
| 4 | import { projects } from '../db/schema.js'; |
| 5 | import { adminStats } from './admin.js'; |
| 6 | import { listDeploys } from './deploy-history.js'; |
| 7 | import { listIncidents } from './incidents.js'; |
| 8 | import { getHealthSummary, type HealthSummary } from './platform-health.js'; |
| 9 | |
| 10 | /** |
| 11 | * The admin landing overview payload. This is a CONTRACT with |
| 12 | * apps/web/(admin)/admin/overview-client.tsx (the `Overview` type). Every |
| 13 | * number here is REAL or an honest null: |
| 14 | * - billing subscribers/mrr/currency/planMix.churn are null while Mavi Pay |
| 15 | * is not connected. planMix (free/pro/team) IS real — a live count of |
| 16 | * projects grouped by tier. |
| 17 | * - health is getHealthSummary() (checks + host). |
| 18 | * - openIncidents counts active (unresolved) incidents. |
| 19 | * - recentDeploys is the deploy-history table, last 8 across services. |
| 20 | * - counts.projects/users from adminStats(). |
| 21 | */ |
| 22 | export interface AdminOverview { |
| 23 | billing: { |
| 24 | subscribers: number | null; |
| 25 | mrr: number | null; |
| 26 | currency: string | null; |
| 27 | planMix: { free: number; pro: number; team: number } | null; |
| 28 | churn30d: number | null; |
| 29 | }; |
| 30 | health: HealthSummary; |
| 31 | openIncidents: number; |
| 32 | recentDeploys: Array<{ |
| 33 | id: string; |
| 34 | service: string; |
| 35 | buildSha: string; |
| 36 | buildAt: string | null; |
| 37 | env: string; |
| 38 | bootedAt: string; |
| 39 | }>; |
| 40 | counts: { projects: number; users: number }; |
| 41 | } |
| 42 | |
| 43 | /** Live count of non-deleted projects grouped by tier. */ |
| 44 | async function planMixFromProjects(): Promise<{ free: number; pro: number; team: number }> { |
| 45 | const db = getDb(); |
| 46 | const rows = await db |
| 47 | .select({ tier: projects.tier, count: sql<number>`count(*)::int` }) |
| 48 | .from(projects) |
| 49 | .where(isNull(projects.deletedAt)) |
| 50 | .groupBy(projects.tier); |
| 51 | const mix = { free: 0, pro: 0, team: 0 }; |
| 52 | for (const r of rows) { |
| 53 | if (r.tier === 'free' || r.tier === 'pro' || r.tier === 'team') { |
| 54 | mix[r.tier] = r.count; |
| 55 | } |
| 56 | } |
| 57 | return mix; |
| 58 | } |
| 59 | |
| 60 | export async function getAdminOverview(): Promise<AdminOverview> { |
| 61 | const [planMix, health, activeIncidents, deploys, stats] = await Promise.all([ |
| 62 | planMixFromProjects(), |
| 63 | getHealthSummary(), |
| 64 | listIncidents({ activeOnly: true, limit: 200 }), |
| 65 | listDeploys({ limit: 8 }), |
| 66 | adminStats(), |
| 67 | ]); |
| 68 | |
| 69 | return { |
| 70 | billing: { |
| 71 | // Mavi Pay is not connected — no subscriber/MRR/churn source yet. Honest |
| 72 | // nulls rather than fabricated revenue. planMix is the one real signal. |
| 73 | subscribers: null, |
| 74 | mrr: null, |
| 75 | currency: null, |
| 76 | planMix, |
| 77 | churn30d: null, |
| 78 | }, |
| 79 | health, |
| 80 | openIncidents: activeIncidents.length, |
| 81 | recentDeploys: deploys.map((d) => ({ |
| 82 | id: d.id, |
| 83 | service: d.service, |
| 84 | buildSha: d.buildSha, |
| 85 | buildAt: d.buildAt, |
| 86 | env: d.env, |
| 87 | bootedAt: d.bootedAt.toISOString(), |
| 88 | })), |
| 89 | counts: { projects: stats.projects, users: stats.users }, |
| 90 | }; |
| 91 | } |