feat(dashboard): cache snapshot data and add credits activity analytics

This commit is contained in:
Matthias
2026-04-08 12:43:58 +02:00
parent 96d9c895ad
commit 22ec672f8e
15 changed files with 996 additions and 40 deletions

102
convex/dashboard.ts Normal file
View File

@@ -0,0 +1,102 @@
import { query } from "./_generated/server";
import { optionalAuth } from "./helpers";
import { prioritizeRecentCreditTransactions } from "../lib/credits-activity";
import { MONTHLY_TIER_CREDITS, normalizeBillingTier } from "../lib/tier-credits";
const DEFAULT_TIER = "free" as const;
const DEFAULT_SUBSCRIPTION_STATUS = "active" as const;
export const getSnapshot = query({
args: {},
handler: async (ctx) => {
const user = await optionalAuth(ctx);
if (!user) {
return {
balance: { balance: 0, reserved: 0, available: 0, monthlyAllocation: 0 },
subscription: {
tier: DEFAULT_TIER,
status: DEFAULT_SUBSCRIPTION_STATUS,
currentPeriodEnd: undefined,
},
usageStats: {
monthlyUsage: 0,
totalGenerations: 0,
monthlyCredits: MONTHLY_TIER_CREDITS[DEFAULT_TIER],
},
recentTransactions: [],
canvases: [],
generatedAt: Date.now(),
};
}
const [balanceRow, subscriptionRow, usageTransactions, recentTransactionsRaw, canvases] =
await Promise.all([
ctx.db
.query("creditBalances")
.withIndex("by_user", (q) => q.eq("userId", user.userId))
.unique(),
ctx.db
.query("subscriptions")
.withIndex("by_user", (q) => q.eq("userId", user.userId))
.order("desc")
.first(),
ctx.db
.query("creditTransactions")
.withIndex("by_user_type", (q) => q.eq("userId", user.userId).eq("type", "usage"))
.order("desc")
.collect(),
ctx.db
.query("creditTransactions")
.withIndex("by_user", (q) => q.eq("userId", user.userId))
.order("desc")
.take(80),
ctx.db
.query("canvases")
.withIndex("by_owner_updated", (q) => q.eq("ownerId", user.userId))
.order("desc")
.collect(),
]);
const tier = normalizeBillingTier(subscriptionRow?.tier);
const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime();
let monthlyUsage = 0;
let totalGenerations = 0;
for (const transaction of usageTransactions) {
if (transaction._creationTime < monthStart) {
break;
}
if (transaction.status === "committed") {
monthlyUsage += Math.abs(transaction.amount);
totalGenerations += 1;
}
}
const balance = {
balance: balanceRow?.balance ?? 0,
reserved: balanceRow?.reserved ?? 0,
available: (balanceRow?.balance ?? 0) - (balanceRow?.reserved ?? 0),
monthlyAllocation: balanceRow?.monthlyAllocation ?? MONTHLY_TIER_CREDITS[tier],
};
return {
balance,
subscription: {
tier: subscriptionRow?.tier ?? DEFAULT_TIER,
status: subscriptionRow?.status ?? DEFAULT_SUBSCRIPTION_STATUS,
currentPeriodEnd: subscriptionRow?.currentPeriodEnd,
},
usageStats: {
monthlyUsage,
totalGenerations,
monthlyCredits: MONTHLY_TIER_CREDITS[tier],
},
recentTransactions: prioritizeRecentCreditTransactions(recentTransactionsRaw, 20),
canvases,
generatedAt: Date.now(),
};
},
});