diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index 676d0c9..692e54c 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -1,12 +1,12 @@
import { Metadata } from 'next';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
-import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags';
+import { isBunnyUploadsFeatureEnabled, isStripeBillingEnabled } from '@/lib/feature-flags';
import { redirect } from 'next/navigation';
-import { getCachedBunnyStorageStats, getCachedTotalStorage } from '@/lib/admin-stats';
+import { getCachedBunnyStorageStats, getCachedTotalStorage, getCachedStripeStats } from '@/lib/admin-stats';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { RefreshR2StatsButton } from '@/components/admin/refresh-r2-stats-button';
-import { Users, Folder, Video, MessageSquare, Mic, HardDrive, Image as ImageIcon, Film, MessageSquareQuote, Star } from 'lucide-react';
+import { Users, Folder, Video, MessageSquare, Mic, HardDrive, Image as ImageIcon, Film, MessageSquareQuote, Star, CreditCard, TrendingUp, UserCheck, AlertCircle, UserX } from 'lucide-react';
export const metadata: Metadata = {
title: 'Admin Dashboard | OpenFrame',
@@ -23,6 +23,16 @@ function formatBytes(bytes: number, decimals = 2) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
+function formatMrr(cents: number, currency: string) {
+ const safeCurrency = /^[a-zA-Z]{3}$/.test(currency) ? currency.toUpperCase() : 'USD';
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: safeCurrency,
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(cents / 100);
+}
+
export default async function AdminDashboardPage() {
const session = await auth();
if (!session?.user?.isAdmin) {
@@ -72,9 +82,10 @@ export default async function AdminDashboardPage() {
}
// 2. Storage Stats (Cached)
- const [totalStorageBytes, bunnyStorageStats] = await Promise.all([
+ const [totalStorageBytes, bunnyStorageStats, stripeStats] = await Promise.all([
getCachedTotalStorage(),
getCachedBunnyStorageStats(),
+ getCachedStripeStats(),
]);
return (
@@ -180,6 +191,69 @@ export default async function AdminDashboardPage() {
+
+ {isStripeBillingEnabled() && stripeStats && (
+ <>
+
Billing & Revenue
+
+
+
+ Monthly Recurring Revenue
+
+
+
+ {formatMrr(stripeStats.mrrCents, stripeStats.currency)}
+ Based on active subscriptions
+
+
+
+
+ Active Subscribers
+
+
+
+ {stripeStats.activeSubscribers}
+
+
+
+
+ On Trial
+
+
+
+ {stripeStats.trialingUsers}
+
+
+
+
+ Free Users
+
+
+
+ {stripeStats.freeUsers}
+
+
+
+
+ Past Due
+
+
+
+ {stripeStats.pastDueUsers}
+
+
+
+
+ Canceled
+
+
+
+ {stripeStats.canceledUsers}
+
+
+
+ >
+ )}
);
}
diff --git a/lib/admin-stats.ts b/lib/admin-stats.ts
index a3f6b2f..3600ea3 100644
--- a/lib/admin-stats.ts
+++ b/lib/admin-stats.ts
@@ -2,7 +2,8 @@ import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
import { r2Client, R2_BUCKET_NAME } from '@/lib/r2';
import { ListObjectsV2Command, type ListObjectsV2CommandInput } from '@aws-sdk/client-s3';
-import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags';
+import { isBunnyUploadsFeatureEnabled, isStripeBillingEnabled } from '@/lib/feature-flags';
+import { getStripe, getStripePriceId } from '@/lib/stripe';
import { logError } from '@/lib/logger';
const BUNNY_API_BASE = 'https://video.bunnycdn.com';
@@ -407,3 +408,62 @@ export const getCachedUserDownloadEgress = unstable_cache(
['admin-user-download-egress'],
{ revalidate: STORAGE_CACHE_SECONDS }
);
+
+export interface StripeStats {
+ activeSubscribers: number;
+ trialingUsers: number;
+ pastDueUsers: number;
+ canceledUsers: number;
+ freeUsers: number;
+ mrrCents: number;
+ currency: string;
+}
+
+const STRIPE_STATS_CACHE_SECONDS = 300;
+
+export const getCachedStripeStats = unstable_cache(
+ async (): Promise => {
+ if (!isStripeBillingEnabled()) return null;
+
+ try {
+ const statusCounts = await db.user.groupBy({
+ by: ['subscriptionStatus'],
+ _count: { id: true },
+ });
+
+ const counts: Record = {};
+ for (const row of statusCounts) {
+ const key = row.subscriptionStatus ?? 'UNKNOWN';
+ counts[key] = row._count.id;
+ }
+
+ const activeSubscribers = counts['ACTIVE'] ?? 0;
+ const trialingUsers = counts['TRIALING'] ?? 0;
+ const pastDueUsers = counts['PAST_DUE'] ?? 0;
+ const canceledUsers = counts['CANCELED'] ?? 0;
+ const freeUsers = counts['FREE'] ?? 0;
+
+ let mrrCents = 0;
+ let currency = 'usd';
+
+ try {
+ const stripe = getStripe();
+ const priceId = getStripePriceId();
+ const price = await stripe.prices.retrieve(priceId);
+ const unitAmount = price.unit_amount ?? 0;
+ currency = price.currency ?? 'usd';
+ mrrCents = activeSubscribers * unitAmount;
+ } catch (err) {
+ logError('Failed to fetch Stripe price for MRR calculation:', err);
+ }
+
+ return { activeSubscribers, trialingUsers, pastDueUsers, canceledUsers, freeUsers, mrrCents, currency };
+ } catch (err) {
+ logError('Failed to fetch Stripe stats:', err);
+ return null;
+ }
+ },
+ ['admin-stripe-stats'],
+ { revalidate: STRIPE_STATS_CACHE_SECONDS }
+);
+