fix: batch priorités — thème, scroll, wizard, agents, slides, charts, packs
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m15s
CI / Deploy production (on server) (push) Has been skipped

Anti-flash dark (color-scheme + applyDocumentTheme), scroll pages publiques,
prompts wizard/slides moins génériques, cron agents rattrapage nextRun null,
slides carnet via notebookId, graphiques à 2 crédits + hint, audit dashboard,
packs Stripe marqués non configurés sans price ID.
This commit is contained in:
Antigravity
2026-07-16 20:48:55 +00:00
parent 556a0b2f3f
commit a77f3ffb6e
16 changed files with 240 additions and 39 deletions

View File

@@ -3,7 +3,7 @@ import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { QuotaExceededError } from '@/lib/entitlements'
import { reserveAiUsageOrThrow } from '@/lib/ai-quota'
import { slideGenerateCreditCost, resolveCreditCost } from '@/lib/credits'
import { slideGenerateCreditCost, notebookSlideCreditCost, resolveCreditCost } from '@/lib/credits'
import type { FeatureName } from '@/lib/quota-utils'
import { logAuditEvent, getClientIp } from '@/lib/audit-log'
import {
@@ -42,8 +42,10 @@ export async function POST(req: NextRequest) {
const userId = session.user.id
const body = await req.json()
const { noteId, type, theme, style, language, template, purpose, audience, slideCount } = body as {
noteId: string
const { noteId, notebookId, type, theme, style, language, template, purpose, audience, slideCount } = body as {
noteId?: string
/** Si fourni (slides), génère un deck multi-notes du carnet */
notebookId?: string
type: GenerateType
theme?: string
style?: string
@@ -54,21 +56,47 @@ export async function POST(req: NextRequest) {
slideCount?: number
}
if (!noteId || !type || !TYPE_DEFAULTS[type]) {
if ((!noteId && !notebookId) || !type || !TYPE_DEFAULTS[type]) {
return NextResponse.json({ error: 'Paramètres invalides' }, { status: 400 })
}
if (notebookId && type !== 'slide-generator') {
return NextResponse.json({ error: 'notebookId réservé aux présentations' }, { status: 400 })
}
// Crédits globaux (respecte le BYOK) — slides = 1 + N
let noteCountForPack = 1
if (notebookId) {
noteCountForPack = await prisma.note.count({
where: {
notebookId,
userId,
isArchived: false,
trashedAt: null,
},
})
if (noteCountForPack < 1) {
return NextResponse.json({ error: 'Carnet vide ou introuvable' }, { status: 404 })
}
}
// Crédits globaux (respecte le BYOK) — slides note = 1+N ; carnet = coût notebook
const featureKey = (type === 'slide-generator' ? 'slide_generate' : 'excalidraw_generate') as FeatureName
const creditCost =
type === 'slide-generator'
? slideGenerateCreditCost(slideCount)
? notebookId
? notebookSlideCreditCost({ slideCount, noteCount: noteCountForPack })
: slideGenerateCreditCost(slideCount)
: resolveCreditCost(featureKey)
try {
await reserveAiUsageOrThrow(userId, featureKey, {
amount: creditCost,
slideCount: type === 'slide-generator' ? slideCount : undefined,
metadata: { noteId, type, slideCount: slideCount ?? null },
metadata: {
noteId: noteId ?? null,
notebookId: notebookId ?? null,
type,
slideCount: slideCount ?? null,
noteCount: noteCountForPack,
},
lane: 'chat',
})
} catch (e) {
@@ -86,12 +114,28 @@ export async function POST(req: NextRequest) {
throw e
}
const note = await prisma.note.findFirst({
where: { id: noteId, userId },
select: { id: true, title: true, notebookId: true },
})
if (!note) {
return NextResponse.json({ error: 'Note introuvable' }, { status: 404 })
let note: { id: string; title: string | null; notebookId: string | null } | null = null
if (noteId) {
note = await prisma.note.findFirst({
where: { id: noteId, userId },
select: { id: true, title: true, notebookId: true },
})
if (!note) {
return NextResponse.json({ error: 'Note introuvable' }, { status: 404 })
}
} else if (notebookId) {
const notebook = await prisma.notebook.findFirst({
where: { id: notebookId, userId },
select: { id: true, name: true },
})
if (!notebook) {
return NextResponse.json({ error: 'Carnet introuvable' }, { status: 404 })
}
note = {
id: notebookId,
title: notebook.name,
notebookId,
}
}
const defaults = TYPE_DEFAULTS[type]
@@ -110,6 +154,10 @@ export async function POST(req: NextRequest) {
role = `Génère un deck exécutif de haute qualité (titres d'action, une idée par slide, pas de filler, graphiques uniquement avec vrais chiffres).${recipeHint}`
}
if (!note) {
return NextResponse.json({ error: 'Source introuvable' }, { status: 404 })
}
const agentName = type === 'slide-generator'
? `${isEn ? 'Slides' : 'Présentation'}${(note.title || 'Note').substring(0, 40)}`
: `${isEn ? 'Diagram' : 'Diagramme'}${(note.title || 'Note').substring(0, 40)}`
@@ -136,6 +184,7 @@ export async function POST(req: NextRequest) {
}
}
const sourceNotebookId = notebookId || note.notebookId || undefined
const agent = await prisma.agent.create({
data: {
name: agentName,
@@ -149,8 +198,9 @@ export async function POST(req: NextRequest) {
maxSteps: defaults.maxSteps,
frequency: 'one-shot',
isEnabled: true,
sourceNoteIds: JSON.stringify([noteId]),
targetNotebookId: note.notebookId ?? undefined,
sourceNoteIds: noteId ? JSON.stringify([noteId]) : JSON.stringify([]),
sourceNotebookId: notebookId ? notebookId : undefined,
targetNotebookId: sourceNotebookId,
slideTheme: theme ?? 'keynote',
slideStyle: style ?? 'soft',
userId,
@@ -168,11 +218,22 @@ export async function POST(req: NextRequest) {
userId,
action: 'AI_REQUEST',
resource: featureKey,
metadata: { agentId: agent.id, noteId, featureKey },
metadata: {
agentId: agent.id,
noteId: noteId ?? null,
notebookId: notebookId ?? null,
featureKey,
creditCost,
},
ip: getClientIp(req),
})
return NextResponse.json({ success: true, agentId: agent.id, status: 'running' })
return NextResponse.json({
success: true,
agentId: agent.id,
status: 'running',
creditCost,
})
}
// ─── GET : poll current agent status ──────────────────────────────────────

View File

@@ -47,7 +47,7 @@ export async function POST(request: NextRequest) {
orderBy: { nextRun: 'asc' },
})
// One-time repair: set nextRun for enabled scheduled agents stuck with null
// Repair: set nextRun for enabled scheduled agents stuck with null
const unscheduled = await prisma.agent.findMany({
where: {
isEnabled: true,
@@ -56,6 +56,7 @@ export async function POST(request: NextRequest) {
},
select: {
id: true,
userId: true,
frequency: true,
scheduledTime: true,
scheduledDay: true,
@@ -63,6 +64,7 @@ export async function POST(request: NextRequest) {
},
take: 50,
})
const repairedDue: typeof dueAgents = []
for (const a of unscheduled) {
const nextRun = calculateNextRun({
frequency: a.frequency,
@@ -70,10 +72,32 @@ export async function POST(request: NextRequest) {
scheduledDay: a.scheduledDay,
timezone: a.timezone,
})
await prisma.agent.update({ where: { id: a.id }, data: { nextRun } })
// Si le créneau calculé est déjà passé / immédiat → exécuter ce cycle
const effective = !nextRun || nextRun <= now ? now : nextRun
await prisma.agent.update({ where: { id: a.id }, data: { nextRun: effective } })
if (effective <= now) {
repairedDue.push({
id: a.id,
userId: a.userId,
frequency: a.frequency,
scheduledTime: a.scheduledTime,
scheduledDay: a.scheduledDay,
timezone: a.timezone,
nextRun: effective,
})
}
}
if (dueAgents.length === 0) {
const queue = [...dueAgents]
const seen = new Set(dueAgents.map((a) => a.id))
for (const a of repairedDue) {
if (!seen.has(a.id)) {
queue.push(a)
seen.add(a.id)
}
}
if (queue.length === 0) {
return NextResponse.json({
success: true,
executed: 0,
@@ -83,8 +107,8 @@ export async function POST(request: NextRequest) {
const results: { id: string; success: boolean; error?: string }[] = []
// Execute agents sequentially (max 3 per cycle)
for (const agent of dueAgents.slice(0, 3)) {
// Execute agents sequentially (max 5 per cycle — rattrapage + due)
for (const agent of queue.slice(0, 5)) {
try {
const { executeAgent } = await import('@/lib/ai/services/agent-executor.service')
const result = await executeAgent(agent.id, agent.userId)