fix: images, couverture, slash, agents, wizard, Stripe et admin

- Collage/accès images: ownership path userId + meta legacy, 403 non caché
- Couverture note: explicite (choix/aucune), plus d’auto depuis le corps
- Éditeur: suppression image TipTap, sync immédiate, toolbar réordonnée
- Slash: listes par titre (plus d’index), keywords nettoyés
- Agents: nextRun à la réactivation, cron sans stampede null
- Wizard: titres courts + mode Expert plus robuste
- Stripe checkout hosted fiable; admin métriques live; dark mode IA/settings
- Indexation notes courtes + test unit aligné
This commit is contained in:
Antigravity
2026-07-16 16:58:07 +00:00
parent 4fe31ebc99
commit 45297da333
27 changed files with 1302 additions and 412 deletions

View File

@@ -1,58 +1,184 @@
import { getUsers } from '@/app/actions/admin'
import { AdminMetrics } from '@/components/admin-metrics'
import { Users, Activity, Database, Zap } from 'lucide-react'
import { Users, Activity, Database, Zap, CreditCard, Bot } from 'lucide-react'
import prisma from '@/lib/prisma'
import Link from 'next/link'
async function getAdminDashboardStats() {
const now = new Date()
const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
const [
userCount,
noteCount,
notebookCount,
agentsEnabled,
agentRunsToday,
paidSubs,
recentUsers,
] = await Promise.all([
prisma.user.count().catch(() => 0),
prisma.note.count({ where: { trashedAt: null } }).catch(() => 0),
prisma.notebook.count().catch(() => 0),
prisma.agent.count({ where: { isEnabled: true } }).catch(() => 0),
prisma.agentAction.count({ where: { createdAt: { gte: dayAgo } } }).catch(() => 0),
prisma.subscription.count({
where: { tier: { in: ['PRO', 'BUSINESS', 'ENTERPRISE'] }, status: 'ACTIVE' },
}).catch(() => 0),
prisma.user.findMany({
orderBy: { createdAt: 'desc' },
take: 8,
select: { id: true, name: true, email: true, createdAt: true, role: true },
}).catch(() => []),
])
// AI usage this month
let aiRequestsMonth = 0
try {
aiRequestsMonth = await prisma.usageLog.aggregate({
where: { periodStart: { gte: monthStart } },
_sum: { requestsCount: true },
}).then((r) => r._sum.requestsCount ?? 0)
} catch {
try {
aiRequestsMonth = await prisma.agentAction.count({
where: { createdAt: { gte: monthStart } },
})
} catch {
aiRequestsMonth = 0
}
}
return {
userCount,
noteCount,
notebookCount,
agentsEnabled,
agentRunsToday,
paidSubs,
aiRequestsMonth,
recentUsers,
}
}
export default async function AdminPage() {
const users = await getUsers()
const [users, stats] = await Promise.all([getUsers(), getAdminDashboardStats()])
// Mock metrics data - in a real app, these would come from analytics
const metrics = [
{
title: 'Total Users',
value: users.length,
trend: { value: 12, isPositive: true },
title: 'Utilisateurs',
value: stats.userCount,
trend: undefined as { value: number; isPositive: boolean } | undefined,
icon: <Users className="h-5 w-5 text-primary dark:text-primary-foreground" />,
},
{
title: 'Active Sessions',
value: '24',
trend: { value: 8, isPositive: true },
icon: <Activity className="h-5 w-5 text-green-600 dark:text-green-400" />,
},
{
title: 'Total Notes',
value: '1,234',
trend: { value: 24, isPositive: true },
title: 'Notes',
value: stats.noteCount.toLocaleString('fr-FR'),
icon: <Database className="h-5 w-5 text-purple-600 dark:text-purple-400" />,
},
{
title: 'AI Requests',
value: '856',
trend: { value: 5, isPositive: false },
title: 'Abonnements payants',
value: stats.paidSubs,
icon: <CreditCard className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />,
},
{
title: 'Agents actifs',
value: stats.agentsEnabled,
icon: <Bot className="h-5 w-5 text-blue-600 dark:text-blue-400" />,
},
{
title: 'Exécutions agents (24h)',
value: stats.agentRunsToday,
icon: <Activity className="h-5 w-5 text-green-600 dark:text-green-400" />,
},
{
title: 'Usage IA (mois)',
value: stats.aiRequestsMonth.toLocaleString('fr-FR'),
icon: <Zap className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />,
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Dashboard
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Overview of your application metrics
</p>
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-3">
<div>
<h1 className="text-3xl font-bold text-foreground">
Administration
</h1>
<p className="text-muted-foreground mt-1">
Vue d&apos;ensemble en temps réel {stats.notebookCount} carnets · {users.length} comptes listés
</p>
</div>
<div className="flex flex-wrap gap-2">
<Link
href="/admin/billing"
className="text-xs font-medium px-3 py-2 rounded-lg border border-border bg-card hover:bg-muted transition-colors"
>
Facturation & quotas
</Link>
<Link
href="/admin/ai"
className="text-xs font-medium px-3 py-2 rounded-lg border border-border bg-card hover:bg-muted transition-colors"
>
Config IA
</Link>
<Link
href="/admin/users"
className="text-xs font-medium px-3 py-2 rounded-lg border border-border bg-card hover:bg-muted transition-colors"
>
Utilisateurs
</Link>
<Link
href="/admin/settings"
className="text-xs font-medium px-3 py-2 rounded-lg border border-border bg-card hover:bg-muted transition-colors"
>
Paramètres
</Link>
</div>
</div>
<AdminMetrics metrics={metrics} />
<div className="bg-card rounded-lg shadow-sm overflow-hidden border border-border p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Recent Activity
</h2>
<p className="text-gray-600 dark:text-gray-400">
Recent activity will be displayed here.
</p>
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-card rounded-lg shadow-sm overflow-hidden border border-border p-6">
<h2 className="text-lg font-semibold text-foreground mb-4">
Derniers utilisateurs
</h2>
{stats.recentUsers.length === 0 ? (
<p className="text-muted-foreground text-sm">Aucun utilisateur.</p>
) : (
<ul className="divide-y divide-border">
{stats.recentUsers.map((u) => (
<li key={u.id} className="py-2.5 flex items-center justify-between gap-3 text-sm">
<div className="min-w-0">
<p className="font-medium text-foreground truncate">{u.name || u.email}</p>
<p className="text-xs text-muted-foreground truncate">{u.email}</p>
</div>
<div className="text-right shrink-0">
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">{u.role}</span>
<p className="text-[10px] text-muted-foreground">
{u.createdAt ? new Date(u.createdAt).toLocaleDateString('fr-FR') : '—'}
</p>
</div>
</li>
))}
</ul>
)}
</div>
<div className="bg-card rounded-lg shadow-sm overflow-hidden border border-border p-6">
<h2 className="text-lg font-semibold text-foreground mb-4">
Raccourcis ops
</h2>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>· Configurer les Price IDs Stripe dans <Link href="/admin/billing" className="text-primary underline">Facturation</Link></li>
<li>· Vérifier les quotas IA par feature (BASIC / PRO / BUSINESS)</li>
<li>· Tester les providers dans <Link href="/admin/ai" className="text-primary underline">IA</Link></li>
<li>· CRON_SECRET requis pour agents planifiés (entrypoint Docker)</li>
<li>· Notes indexées via embeddings + fragments (y compris notes courtes)</li>
</ul>
</div>
</div>
</div>
)