diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec7f85..aa79197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.40.0 — 2026-07-17 14:00 + +### Added +- AI Settings now shows a usage panel — AI calls, recipes created, and storage used this month against your tier's limits, with progress bars. + ## 0.39.0 — 2026-07-17 13:30 ### Fixed diff --git a/apps/web/app/(app)/settings/ai/page.tsx b/apps/web/app/(app)/settings/ai/page.tsx index 3e77312..fb80129 100644 --- a/apps/web/app/(app)/settings/ai/page.tsx +++ b/apps/web/app/(app)/settings/ai/page.tsx @@ -1,10 +1,12 @@ import type { Metadata } from "next"; import { headers } from "next/headers"; import { auth } from "@/lib/auth/server"; -import { db, userAiKeys, userModelPrefs, eq } from "@epicure/db"; +import { db, userAiKeys, userModelPrefs, users, tierDefinitions, userUsage, eq, and } from "@epicure/db"; +import { UNLIMITED } from "@/lib/tiers"; import { ByokManager } from "@/components/settings/byok-manager"; import { ModelPrefsForm } from "@/components/settings/model-prefs-form"; -import { getMessages } from "@/lib/i18n/server"; +import { UsageQuotaSection } from "@/components/settings/usage-quota-section"; +import { getMessages, formatMessage } from "@/lib/i18n/server"; export const metadata: Metadata = {}; @@ -13,7 +15,9 @@ export default async function AiSettingsPage() { if (!session) return null; const m = getMessages((session.user as { locale?: string }).locale); - const [aiKeys, modelPrefs] = await Promise.all([ + const currentMonth = new Date().toISOString().slice(0, 7); + + const [aiKeys, modelPrefs, dbUser] = await Promise.all([ db.query.userAiKeys.findMany({ where: eq(userAiKeys.userId, session.user.id), columns: { provider: true }, @@ -21,10 +25,52 @@ export default async function AiSettingsPage() { db.query.userModelPrefs.findFirst({ where: eq(userModelPrefs.userId, session.user.id), }), + // Tier comes from the DB, not the (up to 5-minute-stale) session cookie + // cache, so a just-changed tier's limits show up immediately here. + db.query.users.findFirst({ where: eq(users.id, session.user.id), columns: { tier: true } }), ]); + const [tierDef, usage] = await Promise.all([ + db.query.tierDefinitions.findFirst({ where: eq(tierDefinitions.tier, dbUser?.tier ?? "free") }), + db.query.userUsage.findFirst({ where: and(eq(userUsage.userId, session.user.id), eq(userUsage.month, currentMonth)) }), + ]); + + const asLimit = (n: number | undefined) => (n === undefined || n === UNLIMITED ? null : n); + + const usageMetrics = [ + { + label: m.settings.usage.aiCalls, + used: usage?.aiCallsUsed ?? 0, + limit: asLimit(tierDef?.aiCallsPerMonth), + unlimitedLabel: m.settings.usage.unlimited, + }, + { + label: m.settings.usage.recipes, + used: usage?.recipeCount ?? 0, + limit: asLimit(tierDef?.maxRecipes), + unlimitedLabel: m.settings.usage.unlimited, + }, + { + label: m.settings.usage.storage, + used: usage?.storageUsedMb ?? 0, + limit: asLimit(tierDef?.storageMb), + unit: " MB", + unlimitedLabel: m.settings.usage.unlimited, + }, + ]; + return (
+ {formatMessage(m.settings.usage.description, { month: currentMonth })} +
+