fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m55s
CI / Deploy production (on server) (push) Successful in 36s

Centralise la réserve via ai-quota, corrige admin unavailable (-1), brancher les routes sans quota et le host-pays brainstorm, avec usage-meter élargi, noms de clusters, MCP et ajustements dashboard/insights.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-07-15 20:42:25 +00:00
parent 30da592ba2
commit 4fe31ebc99
75 changed files with 2949 additions and 785 deletions

View File

@@ -7,6 +7,7 @@ import { VALID_FEATURES, getCurrentPeriodKey } from '@/lib/quota-utils'
import {
getAllEntitlementsForAdmin,
invalidateEntitlementCache,
ENTITLEMENT_UNAVAILABLE,
type SubscriptionTier as TierType,
} from '@/lib/plan-entitlements'
import { logAuditEventAsync } from '@/lib/audit-log'
@@ -73,9 +74,20 @@ export async function updatePlanEntitlement(
}
if (mode === 'unavailable') {
await prisma.planEntitlement.deleteMany({
where: { tier: tier as SubscriptionTier, feature },
})
await prisma.planEntitlement.upsert({
where: {
tier_feature: {
tier: tier as SubscriptionTier,
feature,
},
},
update: { limitValue: ENTITLEMENT_UNAVAILABLE },
create: {
tier: tier as SubscriptionTier,
feature,
limitValue: ENTITLEMENT_UNAVAILABLE,
},
});
} else {
await prisma.planEntitlement.upsert({
where: {
@@ -158,7 +170,7 @@ export async function updateBillingConfig(data: Record<string, string>) {
async function getUsageOverviewInternal() {
const period = getCurrentPeriodKey()
const periodStart = new Date(`${period}-01`)
const periodStart = new Date(`${period}-01T00:00:00.000Z`)
const aggregated = await prisma.usageLog.groupBy({
by: ['feature'],

View File

@@ -4,6 +4,8 @@ import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { createHash, randomBytes } from 'crypto'
import { assertMcpAccess, isMcpTierAllowed } from '@/lib/mcp-access'
import { getEffectiveTier } from '@/lib/entitlements'
const KEY_PREFIX = 'mcp_key_'
@@ -62,6 +64,8 @@ export async function generateMcpKey(name: string): Promise<{ rawKey: string; in
const session = await auth()
if (!session?.user?.id) throw new Error('Unauthorized')
await assertMcpAccess(session.user.id)
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { id: true, name: true, email: true },
@@ -155,6 +159,13 @@ export type McpServerStatus = {
url: string | null
}
export async function getMcpAccessStatus(): Promise<{ allowed: boolean; tier: string }> {
const session = await auth()
if (!session?.user?.id) return { allowed: false, tier: 'BASIC' }
const tier = await getEffectiveTier(session.user.id)
return { allowed: isMcpTierAllowed(tier), tier }
}
/**
* Get MCP server status — mode and URL.
*/

View File

@@ -5,6 +5,8 @@ import prisma from '@/lib/prisma'
import { getChatProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import { revalidatePath } from 'next/cache'
import { withAiQuota } from '@/lib/ai-quota'
import { QuotaExceededError } from '@/lib/entitlements'
export interface OrganizationNote {
id: string
@@ -134,8 +136,16 @@ Format de réponse JSON attendu:
let rawResponse: string
try {
rawResponse = await provider.generateText(prompt)
rawResponse = await withAiQuota(
session.user.id,
'reformulate',
() => provider.generateText(prompt),
{ lane: 'chat' },
)
} catch (aiErr) {
if (aiErr instanceof QuotaExceededError) {
return { success: false, error: 'Quota IA épuisé pour cette fonctionnalité.' }
}
console.error('[organize-notebook] AI generateText failed:', aiErr)
return { success: false, error: `L'IA n'a pas pu répondre : ${(aiErr as Error).message}` }
}