This commit is contained in:
nBiqoz
2025-10-06 19:16:20 +02:00
parent 96dd721fcb
commit 0f2adca44a
23 changed files with 1569 additions and 248 deletions

View File

@@ -2,6 +2,7 @@ import { Suspense } from "react";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { UsageAnalytics } from "@/components/dashboard/usage-analytics"; import { UsageAnalytics } from "@/components/dashboard/usage-analytics";
import { RecentTransactions } from "@/components/dashboard/recent-transactions"; import { RecentTransactions } from "@/components/dashboard/recent-transactions";
import { BarChart3 } from "lucide-react"; import { BarChart3 } from "lucide-react";
function AnalyticsSkeleton() { function AnalyticsSkeleton() {

View File

@@ -0,0 +1,124 @@
import { NextResponse } from "next/server";
import { getDatabase } from "@/lib/db/mongodb";
export async function POST() {
try {
const db = await getDatabase();
const CREDITS_TO_ADD = 5000000; // 5 millions de tokens
console.log(`🚀 DÉBUT: Ajout de ${CREDITS_TO_ADD.toLocaleString()} crédits à tous les utilisateurs`);
// Récupérer tous les utilisateurs existants
const users = await db.collection("users").find({}).toArray();
console.log(`👥 Utilisateurs trouvés: ${users.length}`);
if (users.length === 0) {
return NextResponse.json({
success: false,
message: "Aucun utilisateur trouvé"
});
}
// Récupérer toutes les balances existantes
const existingBalances = await db.collection("balances").find({}).toArray();
const existingBalanceUserIds = new Set(
existingBalances.map(balance => balance.user.toString())
);
console.log(`💰 Balances existantes: ${existingBalances.length}`);
let updatedCount = 0;
let createdCount = 0;
let totalCreditsAdded = 0;
// Pour chaque utilisateur
for (const user of users) {
const userId = user._id;
if (existingBalanceUserIds.has(userId.toString())) {
// Utilisateur a déjà une balance - ajouter les crédits
const updateResult = await db.collection("balances").updateOne(
{ user: userId },
{
$inc: { tokenCredits: CREDITS_TO_ADD },
$set: { lastRefill: new Date() }
}
);
if (updateResult.modifiedCount > 0) {
updatedCount++;
totalCreditsAdded += CREDITS_TO_ADD;
console.log(`✅ Crédits ajoutés pour l'utilisateur: ${user.email || user.name}`);
}
} else {
// Utilisateur n'a pas de balance - créer une nouvelle balance
const newBalance = {
user: userId,
tokenCredits: CREDITS_TO_ADD,
autoRefillEnabled: false,
lastRefill: new Date(),
refillAmount: 0,
refillIntervalUnit: "month",
refillIntervalValue: 1,
__v: 0
};
await db.collection("balances").insertOne(newBalance);
createdCount++;
totalCreditsAdded += CREDITS_TO_ADD;
console.log(`🆕 Nouvelle balance créée pour: ${user.email || user.name}`);
}
}
console.log(`✅ TERMINÉ:`);
console.log(`- Balances mises à jour: ${updatedCount}`);
console.log(`- Nouvelles balances créées: ${createdCount}`);
console.log(`- Total crédits ajoutés: ${totalCreditsAdded.toLocaleString()}`);
return NextResponse.json({
success: true,
statistics: {
totalUsers: users.length,
updatedBalances: updatedCount,
createdBalances: createdCount,
creditsPerUser: CREDITS_TO_ADD,
totalCreditsAdded
},
message: `${CREDITS_TO_ADD.toLocaleString()} crédits ajoutés à ${users.length} utilisateurs (${updatedCount} mis à jour, ${createdCount} créés)`
});
} catch (error) {
console.error("Erreur lors de l'ajout des crédits:", error);
return NextResponse.json({
success: false,
error: "Erreur serveur lors de l'ajout des crédits"
}, { status: 500 });
}
}
// Endpoint pour vérifier les crédits actuels
export async function GET() {
try {
const db = await getDatabase();
const users = await db.collection("users").find({}).toArray();
const balances = await db.collection("balances").find({}).toArray();
const totalCredits = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
const averageCredits = balances.length > 0 ? totalCredits / balances.length : 0;
return NextResponse.json({
statistics: {
totalUsers: users.length,
totalBalances: balances.length,
totalCredits,
averageCredits: Math.round(averageCredits),
usersWithoutBalance: users.length - balances.length
}
});
} catch (error) {
console.error("Erreur lors de la récupération des statistiques:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@@ -0,0 +1,104 @@
import { NextResponse } from "next/server";
import { getDatabase } from "@/lib/db/mongodb";
export async function POST() {
try {
const db = await getDatabase();
// Récupérer tous les utilisateurs existants
const users = await db.collection("users").find({}).toArray();
const userIds = new Set(users.map(user => user._id.toString()));
// Récupérer toutes les balances
const balances = await db.collection("balances").find({}).toArray();
// Identifier les balances fantômes
const phantomBalances = balances.filter(balance =>
!userIds.has(balance.user.toString())
);
// Calculer les statistiques avant nettoyage
const totalBalances = balances.length;
const phantomCount = phantomBalances.length;
const phantomCredits = phantomBalances.reduce(
(sum, balance) => sum + (balance.tokenCredits || 0),
0
);
console.log(`🗑️ SUPPRESSION: ${phantomCount} balances fantômes détectées`);
console.log(`💰 CRÉDITS FANTÔMES: ${phantomCredits}`);
// SUPPRESSION DÉFINITIVE des balances fantômes
const deleteResult = await db.collection("balances").deleteMany({
user: { $nin: Array.from(userIds) }
});
console.log(`✅ SUPPRIMÉES: ${deleteResult.deletedCount} balances`);
return NextResponse.json({
success: true,
statistics: {
totalBalances,
phantomCount,
phantomCredits,
cleanedCount: deleteResult.deletedCount
},
message: `${deleteResult.deletedCount} balances fantômes supprimées définitivement`
});
} catch (error) {
console.error("Erreur lors du nettoyage des balances:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
// Endpoint pour analyser sans nettoyer
export async function GET() {
try {
const db = await getDatabase();
const users = await db.collection("users").find({}).toArray();
const userIds = new Set(users.map(user => user._id.toString()));
const balances = await db.collection("balances").find({}).toArray();
const phantomBalances = balances.filter(balance =>
!userIds.has(balance.user.toString())
);
const phantomCredits = phantomBalances.reduce(
(sum, balance) => sum + (balance.tokenCredits || 0),
0
);
// Analyser les doublons aussi
const userCounts = new Map<string, number>();
balances.forEach(balance => {
const userId = balance.user.toString();
userCounts.set(userId, (userCounts.get(userId) || 0) + 1);
});
const duplicates = Array.from(userCounts.entries())
.filter(([, count]) => count > 1)
.map(([userId, count]) => ({
userId,
count,
isPhantom: !userIds.has(userId)
}));
return NextResponse.json({
analysis: {
totalBalances: balances.length,
totalUsers: users.length,
phantomBalances: phantomBalances.length,
phantomCredits,
duplicateUsers: duplicates.length,
duplicates: duplicates.slice(0, 10) // Premiers 10 exemples
}
});
} catch (error) {
console.error("Erreur lors de l'analyse des balances:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@@ -13,21 +13,21 @@ export async function GET() {
db.collection("balances").find({}).toArray(), db.collection("balances").find({}).toArray(),
]); ]);
// Calculer les utilisateurs actifs (dernière semaine) // Calculer les utilisateurs actifs (30 derniers jours)
const oneWeekAgo = new Date(); const thirtyDaysAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const activeUsers = users.filter((user) => { const activeUsers = users.filter((user) => {
const lastActivity = new Date(user.updatedAt || user.createdAt); const lastActivity = new Date(user.updatedAt || user.createdAt);
return lastActivity >= oneWeekAgo; return lastActivity >= thirtyDaysAgo;
}).length; }).length;
// Calculer les administrateurs // Calculer les administrateurs
const totalAdmins = users.filter(user => user.role === 'ADMIN').length; const totalAdmins = users.filter(user => user.role === 'ADMIN').length;
// Calculer les conversations actives (dernière semaine) // Calculer les conversations actives (30 derniers jours)
const activeConversations = conversations.filter((conv) => { const activeConversations = conversations.filter((conv) => {
const lastActivity = new Date(conv.updatedAt || conv.createdAt); const lastActivity = new Date(conv.updatedAt || conv.createdAt);
return lastActivity >= oneWeekAgo; return lastActivity >= thirtyDaysAgo;
}).length; }).length;
// Calculer le total des messages // Calculer le total des messages
@@ -41,32 +41,27 @@ export async function GET() {
return sum + Math.abs(Number(transaction.rawAmount) || 0); return sum + Math.abs(Number(transaction.rawAmount) || 0);
}, 0); }, 0);
// Calculer le total des crédits depuis balances // Calculer le total des crédits
const totalCredits = balances.reduce((sum, balance) => { const totalCreditsUsed = balances.reduce(
return sum + (Number(balance.tokenCredits) || 0); (sum, balance) => sum + (balance.tokenCredits || 0),
}, 0); 0
);
// Récupérer les transactions récentes (dernières 10) // Récupérer les transactions récentes (dernières 10)
const recentTransactions = transactions const recentTransactions = transactions
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, 10) .slice(0, 10);
.map(transaction => ({
_id: transaction._id,
description: `Transaction ${transaction.tokenType} - ${transaction.model}`,
amount: transaction.rawAmount,
type: transaction.rawAmount > 0 ? 'credit' : 'debit',
createdAt: transaction.createdAt
}));
return NextResponse.json({ return NextResponse.json({
totalUsers: users.length, totalUsers: users.length,
activeUsers, activeUsers,
totalAdmins, totalAdmins,
totalCredits, totalCredits: totalCreditsUsed,
activeConversations, activeConversations,
totalMessages: totalMessages, totalMessages,
totalTokensConsumed, totalTokensConsumed,
recentTransactions totalCreditsUsed,
recentTransactions,
}); });
} catch (error) { } catch (error) {
console.error("Erreur lors du calcul des métriques:", error); console.error("Erreur lors du calcul des métriques:", error);

View File

@@ -8,6 +8,22 @@ export async function GET() {
// Récupérer toutes les transactions // Récupérer toutes les transactions
const transactions = await db.collection("transactions").find({}).toArray(); const transactions = await db.collection("transactions").find({}).toArray();
console.log(`Total transactions trouvées: ${transactions.length}`);
// Vérifier les champs de date disponibles dans les transactions
if (transactions.length > 0) {
const sampleTransaction = transactions[0];
console.log("Exemple de transaction:", {
_id: sampleTransaction._id,
createdAt: sampleTransaction.createdAt,
updatedAt: sampleTransaction.updatedAt,
date: sampleTransaction.date,
timestamp: sampleTransaction.timestamp,
rawAmount: sampleTransaction.rawAmount,
model: sampleTransaction.model
});
}
// Calculer les tokens par jour (7 derniers jours) // Calculer les tokens par jour (7 derniers jours)
const dailyStats = []; const dailyStats = [];
const today = new Date(); const today = new Date();
@@ -22,14 +38,44 @@ export async function GET() {
nextDate.setDate(nextDate.getDate() + 1); nextDate.setDate(nextDate.getDate() + 1);
const dayTransactions = transactions.filter(transaction => { const dayTransactions = transactions.filter(transaction => {
const transactionDate = new Date(transaction.createdAt); // Essayer différents champs de date
let transactionDate = null;
if (transaction.createdAt) {
transactionDate = new Date(transaction.createdAt);
} else if (transaction.updatedAt) {
transactionDate = new Date(transaction.updatedAt);
} else if (transaction.date) {
transactionDate = new Date(transaction.date);
} else if (transaction.timestamp) {
transactionDate = new Date(transaction.timestamp);
} else if (transaction._id && transaction._id.getTimestamp) {
// Utiliser le timestamp de l'ObjectId MongoDB
transactionDate = transaction._id.getTimestamp();
}
if (!transactionDate || isNaN(transactionDate.getTime())) {
return false;
}
return transactionDate >= date && transactionDate < nextDate; return transactionDate >= date && transactionDate < nextDate;
}); });
const totalTokens = dayTransactions.reduce((sum, transaction) => { const totalTokens = dayTransactions.reduce((sum, transaction) => {
return sum + Math.abs(Number(transaction.rawAmount) || 0); // Essayer différents champs pour les tokens
let tokens = 0;
if (transaction.rawAmount) {
tokens = Math.abs(Number(transaction.rawAmount) || 0);
} else if (transaction.amount) {
tokens = Math.abs(Number(transaction.amount) || 0);
} else if (transaction.tokens) {
tokens = Math.abs(Number(transaction.tokens) || 0);
}
return sum + tokens;
}, 0); }, 0);
console.log(`${dayNames[date.getDay()]} (${date.toISOString().split('T')[0]}): ${dayTransactions.length} transactions, ${totalTokens} tokens`);
dailyStats.push({ dailyStats.push({
name: dayNames[date.getDay()], name: dayNames[date.getDay()],
value: totalTokens value: totalTokens
@@ -40,9 +86,19 @@ export async function GET() {
const modelStats = new Map<string, number>(); const modelStats = new Map<string, number>();
transactions.forEach(transaction => { transactions.forEach(transaction => {
const model = transaction.model || "Inconnu"; const model = transaction.model || transaction.modelName || "Inconnu";
const tokens = Math.abs(Number(transaction.rawAmount) || 0); let tokens = 0;
if (transaction.rawAmount) {
tokens = Math.abs(Number(transaction.rawAmount) || 0);
} else if (transaction.amount) {
tokens = Math.abs(Number(transaction.amount) || 0);
} else if (transaction.tokens) {
tokens = Math.abs(Number(transaction.tokens) || 0);
}
if (tokens > 0) {
modelStats.set(model, (modelStats.get(model) || 0) + tokens); modelStats.set(model, (modelStats.get(model) || 0) + tokens);
}
}); });
// Convertir en array et trier par usage // Convertir en array et trier par usage
@@ -50,6 +106,12 @@ export async function GET() {
.map(([name, value]) => ({ name, value })) .map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value); .sort((a, b) => b.value - a.value);
console.log("Statistiques calculées:", {
dailyStats,
totalModels: modelData.length,
topModel: modelData[0]
});
return NextResponse.json({ return NextResponse.json({
dailyTokens: dailyStats, dailyTokens: dailyStats,
modelDistribution: modelData modelDistribution: modelData

View File

@@ -8,16 +8,16 @@ export async function GET() {
// Récupérer tous les utilisateurs // Récupérer tous les utilisateurs
const users = await db.collection("users").find({}).toArray(); const users = await db.collection("users").find({}).toArray();
// Calculer les utilisateurs actifs (dernière semaine) // Calculer les utilisateurs actifs (30 derniers jours)
const oneWeekAgo = new Date(); const thirtyDaysAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
let activeUsers = 0; let activeUsers = 0;
let inactiveUsers = 0; let inactiveUsers = 0;
users.forEach(user => { users.forEach(user => {
const lastActivity = new Date(user.updatedAt || user.createdAt); const lastActivity = new Date(user.updatedAt || user.createdAt);
if (lastActivity >= oneWeekAgo) { if (lastActivity >= thirtyDaysAgo) {
activeUsers++; activeUsers++;
} else { } else {
inactiveUsers++; inactiveUsers++;

194
app/login/page.tsx Normal file
View File

@@ -0,0 +1,194 @@
"use client";
import { useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Loader2, Mail, Lock, Shield } from "lucide-react";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const router = useRouter();
// Fonction pour traduire les erreurs Supabase en français
const getErrorMessage = (error: string) => {
const errorMessages: { [key: string]: string } = {
"Invalid login credentials": "Identifiants de connexion invalides",
"Email not confirmed": "Email non confirmé",
"Too many requests": "Trop de tentatives de connexion",
"User not found": "Utilisateur non trouvé",
"Invalid email": "Adresse email invalide",
"Password should be at least 6 characters": "Le mot de passe doit contenir au moins 6 caractères",
"Email rate limit exceeded": "Limite de tentatives dépassée",
"Invalid email or password": "Email ou mot de passe incorrect",
"Account not found": "Compte non trouvé",
"Invalid credentials": "Identifiants incorrects",
"Authentication failed": "Échec de l'authentification",
"Access denied": "Accès refusé",
"Unauthorized": "Non autorisé",
};
// Chercher une correspondance exacte
if (errorMessages[error]) {
return errorMessages[error];
}
// Chercher une correspondance partielle
for (const [englishError, frenchError] of Object.entries(errorMessages)) {
if (error.toLowerCase().includes(englishError.toLowerCase())) {
return frenchError;
}
}
// Message par défaut si aucune correspondance
return "Erreur de connexion. Vérifiez vos identifiants.";
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
// Validation côté client
if (!email || !password) {
setError("Veuillez remplir tous les champs");
setLoading(false);
return;
}
if (!email.includes("@")) {
setError("Veuillez entrer une adresse email valide");
setLoading(false);
return;
}
const supabase = createClient();
try {
const { data, error: authError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (authError) {
setError(getErrorMessage(authError.message));
return;
}
if (data.user) {
router.push("/");
router.refresh();
}
} catch (error) {
console.error('Login error:', error);
setError("Une erreur inattendue est survenue. Veuillez réessayer.");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg border border-gray-200">
<CardHeader className="space-y-6 pb-8">
<div className="flex justify-center">
<div className="relative w-16 h-16 rounded-full bg-gray-100 p-2">
<Image
src="/img/logo.png"
alt="Logo"
width={48}
height={48}
className="rounded-full"
/>
</div>
</div>
<div className="text-center space-y-2">
<CardTitle className="text-2xl font-bold text-gray-900 flex items-center justify-center gap-2">
<Shield className="w-6 h-6 text-gray-700" />
Admin Dashboard
</CardTitle>
<p className="text-gray-600 text-sm">
Connectez-vous pour accéder au tableau de bord
</p>
</div>
</CardHeader>
<CardContent className="space-y-6">
{error && (
<Alert className="border-red-200 bg-red-50">
<AlertDescription className="text-red-700">
{error}
</AlertDescription>
</Alert>
)}
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium text-gray-700">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="email"
type="email"
placeholder="admin@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="pl-10 border-gray-300 focus:border-gray-900 focus:ring-gray-900"
required
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium text-gray-700">
Mot de passe
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 border-gray-300 focus:border-gray-900 focus:ring-gray-900"
required
/>
</div>
</div>
<Button
type="submit"
className="w-full bg-gray-900 hover:bg-gray-800 text-white font-medium py-2.5 transition-colors"
disabled={loading}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connexion en cours...
</>
) : (
"Se connecter"
)}
</Button>
</form>
<div className="text-center">
<p className="text-xs text-gray-500">
Accès réservé aux administrateurs autorisés
</p>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,5 +1,14 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Database, Server, Settings } from "lucide-react";
import AddCredits from "@/components/dashboard/add-credits";
export default function SettingsPage() { export default function SettingsPage() {
return ( return (
@@ -7,65 +16,73 @@ export default function SettingsPage() {
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Paramètres</h1> <h1 className="text-3xl font-bold tracking-tight">Paramètres</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Configuration du dashboard Cercle GPT Configuration et maintenance du système
</p> </p>
</div> </div>
{/* Informations système */}
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Connexion MongoDB</CardTitle> <CardTitle className="text-sm font-medium flex items-center gap-2">
<Database className="h-4 w-4" />
Base de données
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground">Statut:</span> <span className="text-sm text-muted-foreground">
Statut MongoDB
</span>
<Badge variant="default">Connecté</Badge> <Badge variant="default">Connecté</Badge>
</div> </div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">
Base de données:
</span>
<span className="text-sm font-mono">Cercle GPT</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">
Collections:
</span>
<span className="text-sm">29 collections</span>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Informations système</CardTitle> <CardTitle className="text-sm font-medium flex items-center gap-2">
<Server className="h-4 w-4" />
Informations système
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Version Next.js: Version Next.js
</span> </span>
<span className="text-sm">15.5.4</span> <Badge variant="secondary">14.x</Badge>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Version Node.js: Version Node.js
</span> </span>
<span className="text-sm">{process.version}</span> <Badge variant="secondary">18.x</Badge>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Environnement: Environnement
</span> </span>
<Badge variant="outline">{process.env.NODE_ENV}</Badge> <Badge variant="outline">Development</Badge>
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Gestion des crédits */}
<div>
<h2 className="text-2xl font-bold tracking-tight mb-4 flex items-center gap-2">
<Settings className="h-6 w-6" />
Gestion des Crédits
</h2>
<div className="space-y-6">
<AddCredits />
</div>
</div>
</div> </div>
); );
} }

View File

@@ -96,7 +96,7 @@ export function UsersTable() {
{users.map((user) => { {users.map((user) => {
const userCredits = creditsMap.get(user._id) || 0; const userCredits = creditsMap.get(user._id) || 0;
const isActive = new Date(user.updatedAt || user.createdAt) > const isActive = new Date(user.updatedAt || user.createdAt) >
new Date(Date.now() - 5 * 60 * 1000); // 5 minutes en millisecondes new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 jours en millisecondes
return ( return (
<TableRow key={user._id}> <TableRow key={user._id}>
@@ -122,13 +122,13 @@ export function UsersTable() {
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant={isActive ? 'default' : 'destructive'}> <Badge variant={isActive ? 'default' : 'secondary'}>
{isActive ? 'Actif' : 'Inactif'} {isActive ? 'Actif' : 'Inactif'}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{formatDate(new Date(user.createdAt))} {formatDate(user.createdAt)}
</span> </span>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -0,0 +1,188 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Plus, DollarSign, Users, TrendingUp } from "lucide-react";
interface AddCreditsStats {
totalUsers: number;
totalBalances: number;
totalCredits: number;
averageCredits: number;
usersWithoutBalance: number;
}
interface AddCreditsResult {
totalUsers: number;
updatedBalances: number;
createdBalances: number;
creditsPerUser: number;
totalCreditsAdded: number;
}
export default function AddCredits() {
const [stats, setStats] = useState<AddCreditsStats | null>(null);
const [result, setResult] = useState<AddCreditsResult | null>(null);
const [loading, setLoading] = useState(false);
const [analyzing, setAnalyzing] = useState(false);
const analyzeCurrentCredits = async () => {
setAnalyzing(true);
try {
const response = await fetch("/api/add-credits");
const data = await response.json();
if (data.statistics) {
setStats(data.statistics);
}
} catch (error) {
console.error("Erreur lors de l'analyse:", error);
} finally {
setAnalyzing(false);
}
};
const addCreditsToAllUsers = async () => {
if (!confirm("Êtes-vous sûr de vouloir ajouter 5 millions de crédits à TOUS les utilisateurs ? Cette action est irréversible.")) {
return;
}
setLoading(true);
try {
const response = await fetch("/api/add-credits", {
method: "POST"
});
const data = await response.json();
if (data.success) {
setResult(data.statistics);
// Rafraîchir les stats
await analyzeCurrentCredits();
} else {
alert("Erreur: " + (data.error || data.message));
}
} catch (error) {
console.error("Erreur lors de l'ajout des crédits:", error);
alert("Erreur lors de l'ajout des crédits");
} finally {
setLoading(false);
}
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Plus className="h-5 w-5" />
Ajouter des Crédits
</CardTitle>
<CardDescription>
Ajouter 5 millions de tokens à tous les utilisateurs existants
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Bouton d'analyse */}
<div className="flex gap-2">
<Button
onClick={analyzeCurrentCredits}
disabled={analyzing}
variant="outline"
>
{analyzing ? "Analyse..." : "Analyser les crédits actuels"}
</Button>
</div>
{/* Statistiques actuelles */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-blue-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium">Utilisateurs</span>
</div>
<p className="text-2xl font-bold text-blue-600">{stats.totalUsers}</p>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium">Total Crédits</span>
</div>
<p className="text-2xl font-bold text-green-600">
{stats.totalCredits.toLocaleString()}
</p>
</div>
<div className="bg-purple-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-purple-600" />
<span className="text-sm font-medium">Moyenne</span>
</div>
<p className="text-2xl font-bold text-purple-600">
{stats.averageCredits.toLocaleString()}
</p>
</div>
<div className="bg-orange-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-orange-600" />
<span className="text-sm font-medium">Sans Balance</span>
</div>
<p className="text-2xl font-bold text-orange-600">{stats.usersWithoutBalance}</p>
</div>
</div>
)}
{/* Bouton d'ajout de crédits */}
{stats && (
<div className="border-t pt-4">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<h4 className="font-semibold text-yellow-800 mb-2"> Action Importante</h4>
<p className="text-yellow-700 text-sm">
Cette action va ajouter <strong>5,000,000 crédits</strong> à chacun des {stats.totalUsers} utilisateurs.
<br />
Total de crédits qui seront ajoutés: <strong>{(stats.totalUsers * 5000000).toLocaleString()}</strong>
</p>
</div>
<Button
onClick={addCreditsToAllUsers}
disabled={loading}
className="w-full bg-green-600 hover:bg-green-700"
>
{loading ? "Ajout en cours..." : `Ajouter 5M crédits à ${stats.totalUsers} utilisateurs`}
</Button>
</div>
)}
{/* Résultats */}
{result && (
<div className="border-t pt-4">
<h4 className="font-semibold text-green-600 mb-3"> Crédits ajoutés avec succès !</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">Balances mises à jour:</span>
<Badge variant="secondary" className="ml-2">{result.updatedBalances}</Badge>
</div>
<div>
<span className="text-gray-600">Nouvelles balances:</span>
<Badge variant="secondary" className="ml-2">{result.createdBalances}</Badge>
</div>
<div>
<span className="text-gray-600">Crédits par utilisateur:</span>
<Badge variant="secondary" className="ml-2">{result.creditsPerUser.toLocaleString()}</Badge>
</div>
<div>
<span className="text-gray-600">Total ajouté:</span>
<Badge variant="secondary" className="ml-2">{result.totalCreditsAdded.toLocaleString()}</Badge>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -9,14 +9,23 @@ import {
CartesianGrid, CartesianGrid,
Tooltip, Tooltip,
ResponsiveContainer, ResponsiveContainer,
Cell,
} from "recharts"; } from "recharts";
interface ModelDistributionChartProps { interface ModelDistributionChartProps {
title: string; title: string;
subtitle?: string;
data: Array<{ data: Array<{
name: string; name: string;
value: number; value: number;
color?: string;
models?: Array<{
name: string;
value: number;
}>; }>;
}>;
showLegend?: boolean;
totalTokens?: number;
} }
interface TooltipPayload { interface TooltipPayload {
@@ -24,6 +33,11 @@ interface TooltipPayload {
payload: { payload: {
name: string; name: string;
value: number; value: number;
color?: string;
models?: Array<{
name: string;
value: number;
}>;
}; };
} }
@@ -32,63 +46,211 @@ interface CustomTooltipProps {
payload?: TooltipPayload[]; payload?: TooltipPayload[];
} }
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => { // Couleurs par fournisseur selon l'image
if (active && payload && payload.length) { const providerColors: { [key: string]: string } = {
return ( Anthropic: "#7C3AED", // Violet vif
<div style={{ OpenAI: "#059669", // Vert turquoise vif
backgroundColor: "hsl(var(--background))", "Mistral AI": "#D97706", // Orange vif
border: "1px solid hsl(var(--border))", Meta: "#DB2777", // Rose/Magenta vif
borderRadius: "8px", Google: "#2563EB", // Bleu vif
padding: "8px", Cohere: "#0891B2", // Cyan vif
fontSize: "12px" };
}}>
<p style={{ margin: 0, color: "#ff0000" }}> // Fonction pour regrouper les modèles par fournisseur
{`${payload[0].value.toLocaleString()} tokens`} const groupByProvider = (modelData: Array<{ name: string; value: number }>) => {
</p> const providerMap: {
<p style={{ margin: 0, color: "#ff0000" }}> [key: string]: {
{payload[0].payload.name} value: number;
</p> models: Array<{ name: string; value: number }>;
</div> };
); } = {};
modelData.forEach((model) => {
let provider = "";
// Déterminer le fournisseur basé sur le nom du modèle
if (
model.name.toLowerCase().includes("claude") ||
model.name.toLowerCase().includes("anthropic")
) {
provider = "Anthropic";
} else if (
model.name.toLowerCase().includes("gpt") ||
model.name.toLowerCase().includes("openai")
) {
provider = "OpenAI";
} else if (model.name.toLowerCase().includes("mistral")) {
provider = "Mistral AI";
} else if (
model.name.toLowerCase().includes("llama") ||
model.name.toLowerCase().includes("meta")
) {
provider = "Meta";
} else if (
model.name.toLowerCase().includes("palm") ||
model.name.toLowerCase().includes("gemini") ||
model.name.toLowerCase().includes("google")
) {
provider = "Google";
} else if (model.name.toLowerCase().includes("cohere")) {
provider = "Cohere";
} else {
provider = "Autres";
} }
if (!providerMap[provider]) {
providerMap[provider] = { value: 0, models: [] };
}
providerMap[provider].value += model.value;
providerMap[provider].models.push(model);
});
return Object.entries(providerMap).map(([name, data]) => ({
name,
value: data.value,
models: data.models,
color: providerColors[name] || "#6B7280",
}));
};
const CustomTooltip = () => {
return null; return null;
}; };
export function ModelDistributionChart({ export function ModelDistributionChart({
title, title,
subtitle,
data, data,
totalTokens,
}: ModelDistributionChartProps) { }: ModelDistributionChartProps) {
// Si les données sont déjà groupées par fournisseur, les utiliser directement
// Sinon, les regrouper automatiquement
const groupedData = data[0]?.models ? data : groupByProvider(data);
// Créer une liste de tous les modèles avec leurs couleurs
const allModels = groupedData.flatMap((provider) =>
provider.models?.map((model) => ({
name: model.name,
color: provider.color,
value: model.value
})) || []
);
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground"> <CardTitle className="text-sm font-medium text-muted-foreground">
{title} {title}
</CardTitle> </CardTitle>
{subtitle && (
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
)}
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
<ResponsiveContainer width="100%" height={200}> <ResponsiveContainer width="100%" height={200}>
<BarChart data={data}> <BarChart
data={groupedData}
margin={{ top: 10, right: 10, left: 10, bottom: 10 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" /> <CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
<XAxis <XAxis
dataKey="name" dataKey="name"
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
className="text-xs fill-muted-foreground" className="text-xs fill-muted-foreground"
tick={false} angle={-45}
textAnchor="end"
height={60}
interval={0}
/> />
<YAxis <YAxis
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
className="text-xs fill-muted-foreground" className="text-xs fill-muted-foreground"
tickFormatter={(value) => {
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
return value.toString();
}}
/> />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Bar <Bar dataKey="value" radius={[2, 2, 0, 0]}>
dataKey="value" {groupedData.map((entry, index) => (
fill="#000000" <Cell key={`cell-${index}`} fill={entry.color} />
radius={[4, 4, 0, 0]} ))}
/> </Bar>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
{/* Petites cartes légères pour chaque provider */}
<div className="mt-4 grid grid-cols-2 gap-3">
{groupedData.map((item, index) => (
<div
key={index}
className="p-3 rounded-lg border border-muted/20 bg-muted/5 hover:bg-muted/10 transition-colors"
style={{
borderLeftColor: item.color,
borderLeftWidth: "3px",
borderLeftStyle: "solid",
}}
>
<div className="flex items-center gap-2 mb-1">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: item.color }}
></div>
<h3
className="text-sm font-medium"
style={{ color: item.color }}
>
{item.name}
</h3>
</div>
<p className="text-lg font-semibold text-foreground">
{item.value.toLocaleString()}
</p>
<p className="text-xs text-muted-foreground">tokens</p>
</div>
))}
</div>
{/* Total général */}
{totalTokens && (
<div className="mt-4 pt-3 border-t border-muted/20 text-center">
<p className="text-sm text-muted-foreground">
Total général:{" "}
<span className="font-semibold text-foreground">
{totalTokens.toLocaleString()}
</span>{" "}
tokens
</p>
</div>
)}
{/* Légende dynamique des modèles */}
{allModels.length > 0 && (
<div className="mt-4 pt-3 border-t border-muted/20">
<h4 className="text-sm font-medium text-muted-foreground mb-3 text-center">
Modèles utilisés
</h4>
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2">
{allModels
.sort((a, b) => b.value - a.value) // Trier par usage décroissant
.map((model, index) => (
<div key={index} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: model.color }}
></div>
<span className="text-xs text-muted-foreground">
{model.name}
</span>
</div>
))}
</div>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -44,12 +44,12 @@ export function RealUserActivityChart() {
{ {
name: "Utilisateurs actifs", name: "Utilisateurs actifs",
value: activity.activeUsers, value: activity.activeUsers,
color: "#22c55e", // Vert clair pour actifs color: "#000000", // Noir pour actifs
}, },
{ {
name: "Utilisateurs inactifs", name: "Utilisateurs inactifs",
value: activity.inactiveUsers, value: activity.inactiveUsers,
color: "#ef4444", // Rouge pour inactifs color: "#666666", // Gris pour inactifs
}, },
]; ];
@@ -62,7 +62,7 @@ export function RealUserActivityChart() {
Activité des utilisateurs Activité des utilisateurs
</CardTitle> </CardTitle>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Actifs = connectés dans les 7 derniers jours Actifs = connectés dans les 30 derniers jours
</p> </p>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -74,8 +74,10 @@ export function RealUserActivityChart() {
cy="50%" cy="50%"
innerRadius={60} innerRadius={60}
outerRadius={100} outerRadius={100}
paddingAngle={5} paddingAngle={2}
dataKey="value" dataKey="value"
stroke="#ffffff"
strokeWidth={2}
> >
{data.map((entry, index) => ( {data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />
@@ -86,17 +88,20 @@ export function RealUserActivityChart() {
backgroundColor: "hsl(var(--background))", backgroundColor: "hsl(var(--background))",
border: "1px solid hsl(var(--border))", border: "1px solid hsl(var(--border))",
borderRadius: "8px", borderRadius: "8px",
fontSize: "12px"
}} }}
formatter={(value: number) => [ formatter={(value: number) => [
`${value} utilisateurs (${((value / total) * 100).toFixed( `${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
1
)}%)`,
"", "",
]} ]}
/> />
<Legend <Legend
wrapperStyle={{
paddingTop: "20px",
fontSize: "12px"
}}
formatter={(value, entry) => ( formatter={(value, entry) => (
<span style={{ color: entry.color }}> <span style={{ color: entry.color, fontWeight: 500 }}>
{value}: {entry.payload?.value} ( {value}: {entry.payload?.value} (
{((entry.payload?.value / total) * 100).toFixed(1)}%) {((entry.payload?.value / total) * 100).toFixed(1)}%)
</span> </span>

View File

@@ -31,8 +31,8 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
<AreaChart data={data}> <AreaChart data={data}>
<defs> <defs>
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.3}/> <stop offset="5%" stopColor={color} stopOpacity={0.8}/>
<stop offset="95%" stopColor={color} stopOpacity={0}/> <stop offset="95%" stopColor={color} stopOpacity={0.2}/>
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" /> <CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
@@ -46,6 +46,11 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
className="text-xs fill-muted-foreground" className="text-xs fill-muted-foreground"
tickFormatter={(value) => {
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
return value.toString();
}}
/> />
<Tooltip <Tooltip
contentStyle={{ contentStyle={{
@@ -54,12 +59,16 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
borderRadius: '8px', borderRadius: '8px',
fontSize: '12px' fontSize: '12px'
}} }}
formatter={(value: number) => [
value >= 1000 ? `${(value / 1000).toFixed(1)}K tokens` : `${value} tokens`,
'Tokens consommés'
]}
/> />
<Area <Area
type="monotone" type="monotone"
dataKey="value" dataKey="value"
stroke={color} stroke={color}
strokeWidth={2} strokeWidth={3}
fill="url(#colorGradient)" fill="url(#colorGradient)"
/> />
</AreaChart> </AreaChart>

View File

@@ -20,12 +20,12 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
{ {
name: 'Utilisateurs actifs', name: 'Utilisateurs actifs',
value: activeUsers, value: activeUsers,
color: '#22c55e' // Vert clair pour actifs color: '#000000' // Noir pour actifs
}, },
{ {
name: 'Utilisateurs inactifs', name: 'Utilisateurs inactifs',
value: inactiveUsers, value: inactiveUsers,
color: '#ef4444' // Rouge pour inactifs color: '#666666' // Gris pour inactifs
}, },
]; ];
@@ -36,7 +36,7 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
<CardHeader> <CardHeader>
<CardTitle className="text-base font-medium">Activité des utilisateurs</CardTitle> <CardTitle className="text-base font-medium">Activité des utilisateurs</CardTitle>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Actifs = connectés dans les 7 derniers jours Actifs = connectés dans les 30 derniers jours
</p> </p>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -48,8 +48,10 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
cy="50%" cy="50%"
innerRadius={60} innerRadius={60}
outerRadius={100} outerRadius={100}
paddingAngle={5} paddingAngle={2}
dataKey="value" dataKey="value"
stroke="#ffffff"
strokeWidth={2}
> >
{data.map((entry, index) => ( {data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />
@@ -60,16 +62,22 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
backgroundColor: 'hsl(var(--background))', backgroundColor: 'hsl(var(--background))',
border: '1px solid hsl(var(--border))', border: '1px solid hsl(var(--border))',
borderRadius: '8px', borderRadius: '8px',
fontSize: '12px'
}} }}
formatter={(value: number) => [ formatter={(value: number) => [
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`, `${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
'' '',
]} ]}
/> />
<Legend <Legend
wrapperStyle={{
paddingTop: "20px",
fontSize: "12px"
}}
formatter={(value, entry) => ( formatter={(value, entry) => (
<span style={{ color: entry.color }}> <span style={{ color: entry.color, fontWeight: 500 }}>
{value}: {entry.payload?.value} ({((entry.payload?.value / total) * 100).toFixed(1)}%) {value}: {entry.payload?.value} (
{((entry.payload?.value / total) * 100).toFixed(1)}%)
</span> </span>
)} )}
/> />

View File

@@ -11,7 +11,7 @@ export function RealTimeStats() {
if (loading) { if (loading) {
return ( return (
<div className="grid gap-6 md:grid-cols-2"> <div className="space-y-6">
<div className="h-64 bg-muted animate-pulse rounded-lg" /> <div className="h-64 bg-muted animate-pulse rounded-lg" />
<div className="h-64 bg-muted animate-pulse rounded-lg" /> <div className="h-64 bg-muted animate-pulse rounded-lg" />
</div> </div>
@@ -20,7 +20,7 @@ export function RealTimeStats() {
if (error) { if (error) {
return ( return (
<div className="grid gap-6 md:grid-cols-2"> <div className="space-y-6">
<Card> <Card>
<CardContent className="flex items-center justify-center h-64"> <CardContent className="flex items-center justify-center h-64">
<div className="text-center"> <div className="text-center">
@@ -47,7 +47,7 @@ export function RealTimeStats() {
if (!stats) { if (!stats) {
return ( return (
<div className="grid gap-6 md:grid-cols-2"> <div className="space-y-6">
<Card> <Card>
<CardContent className="flex items-center justify-center h-64"> <CardContent className="flex items-center justify-center h-64">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
@@ -67,7 +67,7 @@ export function RealTimeStats() {
} }
return ( return (
<div className="grid gap-6 md:grid-cols-2"> <div className="space-y-6">
<SimpleStatsChart <SimpleStatsChart
title="Tokens consommés par jour" title="Tokens consommés par jour"
data={stats.dailyTokens} data={stats.dailyTokens}

View File

@@ -46,24 +46,20 @@ export function UsageAnalytics() {
setLoading(true); setLoading(true);
// Console log pour débugger les données balances console.log("=== CALCUL DES STATISTIQUES ===");
console.log("=== DONNÉES BALANCES RÉCUPÉRÉES ==="); console.log("Utilisateurs:", users.length);
console.log("Nombre total d'entrées balances:", balances.length); console.log("Conversations:", conversations.length);
console.log("Toutes les entrées balances:", balances); console.log("Transactions:", transactions.length);
console.log("Balances:", balances.length);
// NOUVEAU : Console log pour débugger les utilisateurs // Analyser les doublons dans les balances
console.log("=== DONNÉES UTILISATEURS ===");
console.log("Nombre total d'utilisateurs:", users.length);
console.log("Premiers 5 utilisateurs:", users.slice(0, 5));
// Analyser les doublons
const userCounts = new Map<string, number>(); const userCounts = new Map<string, number>();
balances.forEach(balance => { balances.forEach((balance) => {
const userId = balance.user; const userId = balance.user;
userCounts.set(userId, (userCounts.get(userId) || 0) + 1); userCounts.set(userId, (userCounts.get(userId) || 0) + 1);
}); });
const duplicateUsers = Array.from(userCounts.entries()).filter(([_, count]) => count > 1); const duplicateUsers = Array.from(userCounts.entries()).filter(([, count]) => count > 1);
console.log("Utilisateurs avec plusieurs entrées:", duplicateUsers); console.log("Utilisateurs avec plusieurs entrées:", duplicateUsers);
// Afficher quelques exemples d'entrées // Afficher quelques exemples d'entrées
@@ -73,6 +69,29 @@ export function UsageAnalytics() {
const totalBrut = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0); const totalBrut = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
console.log("Total brut (avec doublons potentiels):", totalBrut); console.log("Total brut (avec doublons potentiels):", totalBrut);
// Ajouter des logs détaillés pour comprendre le problème
console.log("=== DIAGNOSTIC DÉTAILLÉ ===");
// Analyser les doublons
const duplicateDetails = Array.from(userCounts.entries())
.filter(([, count]) => count > 1)
.map(([userId, count]) => {
const userBalances = balances.filter(b => b.user === userId);
const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0);
return {
userId,
count,
totalCredits,
balances: userBalances.map(b => ({
credits: b.tokenCredits,
createdAt: b.createdAt,
updatedAt: b.updatedAt
}))
};
});
console.log("Détails des doublons:", duplicateDetails);
// NOUVEAU : Identifier les utilisateurs fantômes // NOUVEAU : Identifier les utilisateurs fantômes
console.log("=== ANALYSE DES UTILISATEURS FANTÔMES ==="); console.log("=== ANALYSE DES UTILISATEURS FANTÔMES ===");
const userIds = new Set(users.map(user => user._id)); const userIds = new Set(users.map(user => user._id));
@@ -91,40 +110,57 @@ export function UsageAnalytics() {
console.log("Crédits des utilisateurs fantômes:", phantomCredits); console.log("Crédits des utilisateurs fantômes:", phantomCredits);
console.log("Crédits des vrais utilisateurs:", totalBrut - phantomCredits); console.log("Crédits des vrais utilisateurs:", totalBrut - phantomCredits);
// Calculer les utilisateurs actifs (5 dernières minutes) // Analyser les utilisateurs fantômes
const fiveMinutesAgo = new Date(); const phantomDetails = uniquePhantomUsers.map(userId => {
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5); const userBalances = balances.filter(b => b.user === userId);
const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0);
return { userId, totalCredits, count: userBalances.length };
});
console.log("Détails des utilisateurs fantômes:", phantomDetails);
// Calculer les utilisateurs actifs (30 derniers jours)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const activeUsers = users.filter((user) => { const activeUsers = users.filter((user) => {
const lastActivity = new Date(user.updatedAt || user.createdAt); const lastActivity = new Date(user.updatedAt || user.createdAt);
return lastActivity >= fiveMinutesAgo; return lastActivity >= thirtyDaysAgo;
}).length; }).length;
// CORRECTION : Créer une map des crédits par utilisateur en évitant les doublons // CORRECTION AMÉLIORÉE : Créer une map des crédits par utilisateur
const creditsMap = new Map<string, number>(); const creditsMap = new Map<string, number>();
// Grouper les balances par utilisateur // Grouper les balances par utilisateur
const balancesByUser = new Map<string, LibreChatBalance[]>(); const balancesByUser = new Map<string, LibreChatBalance[]>();
balances.forEach((balance) => { balances.forEach((balance) => {
const userId = balance.user; const userId = balance.user;
// Ignorer les utilisateurs fantômes (qui n'existent plus)
if (users.some(user => user._id === userId)) {
if (!balancesByUser.has(userId)) { if (!balancesByUser.has(userId)) {
balancesByUser.set(userId, []); balancesByUser.set(userId, []);
} }
balancesByUser.get(userId)!.push(balance); balancesByUser.get(userId)!.push(balance);
}
}); });
// Pour chaque utilisateur, prendre seulement la dernière entrée // Pour chaque utilisateur, calculer les crédits selon votre logique métier
balancesByUser.forEach((userBalances, userId) => { balancesByUser.forEach((userBalances, userId) => {
if (userBalances.length > 0) { if (userBalances.length > 0) {
// Trier par date de mise à jour (plus récent en premier) // OPTION A: Prendre la balance la plus récente
const sortedBalances = userBalances.sort((a, b) => { const sortedBalances = userBalances.sort((a, b) => {
const aDate = new Date((a.updatedAt as string) || (a.createdAt as string) || 0); const aDate = new Date((a.updatedAt as string) || (a.createdAt as string) || 0);
const bDate = new Date((b.updatedAt as string) || (b.createdAt as string) || 0); const bDate = new Date((b.updatedAt as string) || (b.createdAt as string) || 0);
return bDate.getTime() - aDate.getTime(); return bDate.getTime() - aDate.getTime();
}); });
creditsMap.set(userId, sortedBalances[0].tokenCredits || 0);
// Prendre la plus récente // OPTION B: Sommer toutes les balances (si c'est votre logique)
const latestBalance = sortedBalances[0]; // const totalCredits = userBalances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
creditsMap.set(userId, latestBalance.tokenCredits || 0); // creditsMap.set(userId, totalCredits);
// OPTION C: Prendre la balance avec le plus de crédits
// const maxCredits = Math.max(...userBalances.map(b => b.tokenCredits || 0));
// creditsMap.set(userId, maxCredits);
} }
}); });
@@ -167,12 +203,16 @@ export function UsageAnalytics() {
} }
}); });
// CORRECTION : Calculer le total des crédits depuis la map corrigée // Calculer le total des crédits depuis la map corrigée (sans doublons ni fantômes)
const totalCreditsUsed = Array.from(creditsMap.values()).reduce( const totalCreditsUsed = Array.from(creditsMap.values()).reduce(
(sum, credits) => sum + credits, (sum, credits) => sum + credits,
0 0
); );
console.log("=== RÉSULTATS CORRIGÉS ===");
console.log("Crédits totaux (sans doublons ni fantômes):", totalCreditsUsed);
console.log("Utilisateurs avec crédits:", creditsMap.size);
// Tous les utilisateurs triés par tokens puis conversations // Tous les utilisateurs triés par tokens puis conversations
const allUsers = Array.from(userStats.entries()) const allUsers = Array.from(userStats.entries())
.map(([userId, stats]) => ({ .map(([userId, stats]) => ({
@@ -242,7 +282,7 @@ export function UsageAnalytics() {
<CardContent> <CardContent>
<div className="text-2xl font-bold">{stats.totalUsers}</div> <div className="text-2xl font-bold">{stats.totalUsers}</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{stats.activeUsers} actifs cette semaine {stats.activeUsers} actifs ce mois
</p> </p>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,12 +1,11 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { createClient } from "@/lib/supabase/client";
import { Separator } from "@/components/ui/separator";
import Image from "next/image"; import Image from "next/image";
import { import {
LayoutDashboard, LayoutDashboard,
@@ -21,135 +20,164 @@ import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
BarChart3, BarChart3,
Activity, LogOut,
User,
Mail,
} from "lucide-react"; } from "lucide-react";
import type { User as SupabaseUser } from "@supabase/supabase-js";
interface NavigationItem { const topLevelNavigation = [
name: string;
href: string;
icon: React.ElementType;
badge?: string | null;
}
const navigation: NavigationItem[] = [
{ {
name: "Vue d'ensemble", name: "Dashboard",
href: "/", href: "/",
icon: LayoutDashboard, icon: LayoutDashboard,
badge: null,
}, },
{ {
name: "Analytics", name: "Analytics",
href: "/analytics", href: "/analytics",
icon: BarChart3, icon: BarChart3,
badge: "Nouveau",
}, },
]; ];
const dataNavigation: NavigationItem[] = [ const navigationGroups = [
{ name: "Utilisateurs", href: "/users", icon: Users, badge: null }, {
name: "Données",
items: [
{
name: "Utilisateurs",
href: "/users",
icon: Users,
},
{ {
name: "Conversations", name: "Conversations",
href: "/conversations", href: "/conversations",
icon: MessageSquare, icon: MessageSquare,
badge: null,
}, },
{ name: "Messages", href: "/messages", icon: FileText, badge: null }, {
name: "Messages",
href: "/messages",
icon: FileText,
},
{ {
name: "Transactions", name: "Transactions",
href: "/transactions", href: "/transactions",
icon: CreditCard, icon: CreditCard,
badge: null, },
],
},
{
name: "Système",
items: [
{
name: "Collections",
href: "/collections",
icon: Database,
},
{
name: "Agents",
href: "/agents",
icon: Bot,
},
{
name: "Rôles",
href: "/roles",
icon: Shield,
},
{
name: "Paramètres",
href: "/settings",
icon: Settings,
},
],
}, },
]; ];
const systemNavigation: NavigationItem[] = [
{ name: "Agents", href: "/agents", icon: Bot, badge: null },
{ name: "Rôles", href: "/roles", icon: Shield, badge: null },
{ name: "Collections", href: "/collections", icon: Database, badge: null },
{ name: "Paramètres", href: "/settings", icon: Settings, badge: null },
];
export function Sidebar() { export function Sidebar() {
const [collapsed, setCollapsed] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter();
const [collapsed, setCollapsed] = useState(false);
const [user, setUser] = useState<SupabaseUser | null>(null);
const [loading, setLoading] = useState(true);
const supabase = createClient();
const NavSection = ({ useEffect(() => {
title, const getUser = async () => {
items, try {
showTitle = true, const {
}: { data: { user },
title: string; } = await supabase.auth.getUser();
items: NavigationItem[]; setUser(user);
showTitle?: boolean; } catch (error) {
}) => ( console.error(
<div className="space-y-2"> "Erreur lors de la récupération de l'utilisateur:",
{!collapsed && showTitle && ( error
<h3 className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{title}
</h3>
)}
{items.map((item) => {
const isActive = pathname === item.href;
return (
<Link key={item.name} href={item.href}>
<Button
variant={isActive ? "secondary" : "ghost"}
className={cn(
"w-full justify-start h-9 px-3",
collapsed && "px-2 justify-center",
isActive && "bg-secondary font-medium"
)}
>
<item.icon className={cn("h-4 w-4", collapsed ? "" : "mr-3")} />
{!collapsed && (
<div className="flex items-center justify-between w-full">
<span>{item.name}</span>
{item.badge && (
<Badge variant="secondary" className="text-xs">
{item.badge}
</Badge>
)}
</div>
)}
</Button>
</Link>
);
})}
</div>
); );
setUser(null);
} finally {
setLoading(false);
}
};
getUser();
// Écouter les changements d'authentification
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
setLoading(false);
});
return () => subscription.unsubscribe();
}, [supabase.auth]);
const handleLogout = async () => {
try {
await supabase.auth.signOut();
router.push("/login");
router.refresh();
} catch (error) {
console.error("Erreur lors de la déconnexion:", error);
}
};
// Ne pas afficher la sidebar si l'utilisateur n'est pas connecté ou en cours de chargement
if (loading || !user) {
return null;
}
return ( return (
<div <div
className={cn( className={cn(
"flex flex-col h-screen bg-background border-r border-border transition-all duration-300 ease-in-out", "flex flex-col h-screen bg-white border-r border-gray-200 transition-all duration-300",
collapsed ? "w-16" : "w-64" collapsed ? "w-16" : "w-64"
)} )}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border"> <div className="flex items-center justify-between p-4 border-b border-gray-200 flex-shrink-0">
{!collapsed && ( {!collapsed && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg flex items-center justify-center"> <div className="relative w-8 h-8">
<Image <Image
src="/img/logo.png" src="/img/logo.png"
alt="Cercle GPT Logo" alt="Logo"
width={32} fill
height={32} className="object-contain"
className="rounded-lg"
/> />
</div> </div>
<div> <div>
<h1 className="text-sm font-semibold">Cercle GPT</h1> <h1 className="text-lg font-semibold text-gray-900">
<p className="text-xs text-muted-foreground">Admin Dashboard</p> Cercle GPT
</h1>
<p className="text-xs text-gray-500">Admin Dashboard</p>
</div> </div>
</div> </div>
)} )}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="sm"
onClick={() => setCollapsed(!collapsed)} onClick={() => setCollapsed(!collapsed)}
className="h-8 w-8" className="p-1.5 hover:bg-gray-100"
> >
{collapsed ? ( {collapsed ? (
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
@@ -160,32 +188,112 @@ export function Sidebar() {
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 p-3 space-y-6 overflow-y-auto"> <nav className="flex-1 p-4 space-y-6 overflow-y-auto">
<NavSection title="Dashboard" items={navigation} showTitle={false} /> {/* Navigation de niveau supérieur */}
<div className="space-y-1">
{topLevelNavigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
isActive
? "bg-gray-900 text-white"
: "text-gray-700 hover:bg-gray-100 hover:text-gray-900"
)}
>
<item.icon className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span>{item.name}</span>}
</Link>
);
})}
</div>
{!collapsed && <Separator />} {/* Séparateur */}
<div className="border-t border-gray-200"></div>
<NavSection title="Données" items={dataNavigation} /> {/* Groupes de navigation */}
{navigationGroups.map((group) => (
<div key={group.name}>
{/* Titre du groupe */}
{!collapsed && (
<h3 className="px-3 mb-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
{group.name}
</h3>
)}
{!collapsed && <Separator />} {/* Séparateur visuel quand collapsed */}
{collapsed && (
<div className="mb-2 mx-auto w-8 h-px bg-gray-300"></div>
)}
<NavSection title="Système" items={systemNavigation} /> {/* Items du groupe */}
<div className="space-y-1">
{group.items.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
isActive
? "bg-gray-900 text-white"
: "text-gray-700 hover:bg-gray-100 hover:text-gray-900"
)}
>
<item.icon className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span>{item.name}</span>}
</Link>
);
})}
</div>
</div>
))}
</nav> </nav>
{/* Footer */} {/* Section utilisateur connecté */}
<div className="p-4 border-t border-gray-200 space-y-3 flex-shrink-0 bg-white">
{/* Informations utilisateur */}
<div
className={cn(
"flex items-center space-x-3 px-3 py-2 bg-gray-50 rounded-md",
collapsed && "justify-center"
)}
>
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center">
<User className="h-4 w-4 text-gray-600" />
</div>
</div>
{!collapsed && ( {!collapsed && (
<div className="p-3 border-t border-border">
<div className="flex items-center space-x-3 p-2 rounded-lg bg-muted/50">
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
<Activity className="h-4 w-4 text-primary" />
</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs font-medium">Système en ligne</p> <p className="text-sm font-medium text-gray-900 truncate">
<p className="text-xs text-muted-foreground">Tout fonctionne</p> Administrateur
</div> </p>
<div className="flex items-center space-x-1">
<Mail className="h-3 w-3 text-gray-400" />
<p className="text-xs text-gray-500 truncate">{user.email}</p>
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Bouton de déconnexion */}
<Button
variant="outline"
onClick={handleLogout}
className={cn(
"w-full flex items-center space-x-2 text-sm font-medium border-gray-200 hover:bg-gray-50",
collapsed && "justify-center px-2"
)}
>
<LogOut className="h-4 w-4" />
{!collapsed && <span>Déconnexion</span>}
</Button>
</div>
</div>
); );
} }

46
components/ui/alert.tsx Normal file
View File

@@ -0,0 +1,46 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertDescription }

8
lib/supabase/client.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}

29
lib/supabase/server.ts Normal file
View File

@@ -0,0 +1,29 @@
import { createServerClient as createSupabaseServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createServerClient() {
const cookieStore = await cookies()
return createSupabaseServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet: Array<{ name: string; value: string; options?: Record<string, unknown> }>) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}

65
middleware.ts Normal file
View File

@@ -0,0 +1,65 @@
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet: Array<{ name: string; value: string; options?: Record<string, unknown> }>) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Rafraîchir la session
const { data: { user } } = await supabase.auth.getUser()
// Si l'utilisateur est sur la page de login
if (request.nextUrl.pathname === '/login') {
if (user) {
// Utilisateur connecté, rediriger vers le dashboard
const redirectUrl = new URL('/', request.url)
return NextResponse.redirect(redirectUrl)
}
// Utilisateur non connecté, autoriser l'accès à la page de login
return supabaseResponse
}
// Pour toutes les autres pages, vérifier l'authentification
if (!user) {
// Utilisateur non connecté, rediriger vers login
const redirectUrl = new URL('/login', request.url)
return NextResponse.redirect(redirectUrl)
}
return supabaseResponse
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
'/((?!_next/static|_next/image|favicon.ico|public|img).*)',
],
}

158
package-lock.json generated
View File

@@ -15,6 +15,8 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.58.0",
"@types/mongodb": "^4.0.6", "@types/mongodb": "^4.0.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -1761,6 +1763,115 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@supabase/auth-js": {
"version": "2.72.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.72.0.tgz",
"integrity": "sha512-4+bnUrtTDK1YD0/FCx2YtMiQH5FGu9Jlf4IQi5kcqRwRwqp2ey39V61nHNdH86jm3DIzz0aZKiWfTW8qXk1swQ==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.5.0.tgz",
"integrity": "sha512-SXBx6Jvp+MOBekeKFu+G11YLYPeVeGQl23eYyAG9+Ro0pQ1aIP0UZNIBxHKNHqxzR0L0n6gysNr2KT3841NATw==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/@supabase/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/@supabase/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.21.4",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz",
"integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.15.5",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz",
"integrity": "sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.13",
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"ws": "^8.18.2"
}
},
"node_modules/@supabase/ssr": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.7.0.tgz",
"integrity": "sha512-G65t5EhLSJ5c8hTCcXifSL9Q/ZRXvqgXeNo+d3P56f4U1IxwTqjB64UfmfixvmMcjuxnq2yGqEWVJqUcO+AzAg==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.2"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.43.4"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.2.tgz",
"integrity": "sha512-SiySHxi3q7gia7NBYpsYRu8gyI0NhFwSORMxbZIxJ/zAVkN6QpwDRan158CJ+UdzD4WB/rQMAGRqIJQP+7ccAQ==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.58.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.58.0.tgz",
"integrity": "sha512-Tm1RmQpoAKdQr4/8wiayGti/no+If7RtveVZjHR8zbO7hhQjmPW2Ok5ZBPf1MGkt5c+9R85AVMsTfSaqAP1sUg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@supabase/auth-js": "2.72.0",
"@supabase/functions-js": "2.5.0",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.21.4",
"@supabase/realtime-js": "2.15.5",
"@supabase/storage-js": "2.12.2"
}
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -2154,12 +2265,17 @@
"version": "20.19.19", "version": "20.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
"integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.0", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
@@ -2203,6 +2319,15 @@
"@types/webidl-conversions": "*" "@types/webidl-conversions": "*"
} }
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.45.0", "version": "8.45.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
@@ -3261,6 +3386,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7280,7 +7414,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unrs-resolver": { "node_modules/unrs-resolver": {
@@ -7539,6 +7672,27 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",

View File

@@ -16,6 +16,8 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.58.0",
"@types/mongodb": "^4.0.6", "@types/mongodb": "^4.0.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",