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 (
+
+
+

{m.settings.usage.title}

+

+ {formatMessage(m.settings.usage.description, { month: currentMonth })} +

+
+ +
+

{m.settings.byok.title}

diff --git a/apps/web/components/settings/usage-quota-section.tsx b/apps/web/components/settings/usage-quota-section.tsx new file mode 100644 index 0000000..3ebd00b --- /dev/null +++ b/apps/web/components/settings/usage-quota-section.tsx @@ -0,0 +1,35 @@ +import { Progress } from "@/components/ui/progress"; + +type UsageMetric = { + label: string; + used: number; + limit: number | null; + unit?: string; + unlimitedLabel: string; +}; + +function UsageBar({ label, used, limit, unit, unlimitedLabel }: UsageMetric) { + const pct = limit === null ? 0 : Math.min(100, (used / Math.max(limit, 1)) * 100); + return ( +
+
+ {label} + + {used} + {unit} / {limit === null ? unlimitedLabel : `${limit}${unit ?? ""}`} + +
+ {limit !== null && } +
+ ); +} + +export function UsageQuotaSection({ metrics }: { metrics: UsageMetric[] }) { + return ( +
+ {metrics.map((m) => ( + + ))} +
+ ); +} diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index fd1e104..abd2ab0 100644 --- a/apps/web/lib/changelog.ts +++ b/apps/web/lib/changelog.ts @@ -1,5 +1,5 @@ // Mirrors CHANGELOG.md at the repo root — update both together. -export const APP_VERSION = "0.39.0"; +export const APP_VERSION = "0.40.0"; export type ChangelogEntry = { version: string; @@ -11,6 +11,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.40.0", + date: "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.", + ], + }, { version: "0.39.0", date: "2026-07-17 13:30", diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index ac75f81..84c2d5a 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -325,6 +325,14 @@ "nutrition": "Nutrition", "apiKeys": "API Keys", "webhooks": "Webhooks", + "usage": { + "title": "Usage this month", + "description": "Resets at the start of each month ({month}).", + "aiCalls": "AI calls", + "recipes": "Recipes created", + "storage": "Storage", + "unlimited": "Unlimited" + }, "byok": { "title": "Your API Keys (BYOK)", "description": "Use your own API keys instead of the app's shared quota. Keys are encrypted at rest." diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json index bd23ebd..8900e2b 100644 --- a/apps/web/messages/fr.json +++ b/apps/web/messages/fr.json @@ -325,6 +325,14 @@ "nutrition": "Nutrition", "apiKeys": "Clés API", "webhooks": "Webhooks", + "usage": { + "title": "Utilisation ce mois-ci", + "description": "Réinitialisé au début de chaque mois ({month}).", + "aiCalls": "Appels IA", + "recipes": "Recettes créées", + "storage": "Stockage", + "unlimited": "Illimité" + }, "byok": { "title": "Vos clés API (BYOK)", "description": "Utilisez vos propres clés API au lieu du quota partagé de l'application. Les clés sont chiffrées au repos." diff --git a/apps/web/package.json b/apps/web/package.json index 8fc7853..a81c62f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.39.0", + "version": "0.40.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package.json b/package.json index c024e4f..f85f2ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.39.0", + "version": "0.40.0", "private": true, "scripts": { "dev": "pnpm --filter web dev",