"use client"; import { useState, useMemo, useEffect } from "react"; import { ScrollText, Search, Download, RefreshCw, CheckCircle2, XCircle, Copy, Check, Trash2, Smartphone, Crosshair, Calendar, HardDrive, Users, ExternalLink, Info, Filter, Eye, ShieldAlert, ShieldCheck, Ban, AlertTriangle } from "lucide-react"; import { API_BASE_URL } from "@/lib/api"; import Link from "next/link"; interface UploadLog { id: number; timestamp: string; session_id: string | null; wallet_hash: string | null; image_filename: string | null; json_filename: string | null; file_size: number | null; ip_address: string | null; user_agent: string | null; device_model: string | null; device_os: string | null; target_type: string | null; weapon: string | null; distance_meters: number | null; impacts_count: number; status: string; target_valid?: number; target_status?: string; target_confidence?: number; target_rings_count?: number; target_details?: string | null; error_message: string | null; raw_metadata: string | null; } interface BannedWallet { wallet_hash: string; reason: string; banned_at: string; banned_by: string; } interface LogStats { total_uploads: number; success_count: number; failed_count: number; today_uploads: number; unique_wallets: number; total_bytes: number; latest_upload: string | null; } export default function LogsManager({ initialLogs, initialStats }: { initialLogs: UploadLog[]; initialStats: LogStats | null; }) { const [logs, setLogs] = useState(initialLogs); const [stats, setStats] = useState(initialStats); const [bannedWallets, setBannedWallets] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState<"ALL" | "SUCCESS" | "FAILED" | "INVALID_TARGET" | "BANNED">("ALL"); const [copiedWallet, setCopiedWallet] = useState(null); const [selectedLog, setSelectedLog] = useState(null); const [isLoading, setIsLoading] = useState(false); const [showClearConfirm, setShowClearConfirm] = useState(false); // Moderation state const [banModalWallet, setBanModalWallet] = useState(null); const [banReason, setBanReason] = useState("Images non conformes / Fausse cible"); const [isSubmittingBan, setIsSubmittingBan] = useState(false); const fetchBannedWallets = async () => { try { const res = await fetch(`${API_BASE_URL}/api/moderation/banned`, { cache: 'no-store' }); if (res.ok) { const data = await res.json(); setBannedWallets(data.banned || []); } } catch (e) { console.error("Erreur récupération wallets bannis:", e); } }; useEffect(() => { fetchBannedWallets(); }, []); const fetchLogs = async () => { setIsLoading(true); try { const [logsRes, statsRes] = await Promise.all([ fetch(`${API_BASE_URL}/api/logs?limit=200`, { cache: 'no-store' }), fetch(`${API_BASE_URL}/api/logs/stats`, { cache: 'no-store' }), fetchBannedWallets() ]); if (logsRes.ok) { const logsData = await logsRes.json(); setLogs(logsData.logs || []); } if (statsRes.ok) { const statsData = await statsRes.json(); setStats(statsData.stats || null); } } catch (e) { console.error("Erreur lors de la récupération des logs:", e); } finally { setIsLoading(false); } }; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); setCopiedWallet(text); setTimeout(() => setCopiedWallet(null), 2000); }; const isWalletBanned = (walletHash: string | null) => { if (!walletHash) return false; return bannedWallets.some(b => b.wallet_hash === walletHash); }; const getBannedInfo = (walletHash: string | null) => { if (!walletHash) return null; return bannedWallets.find(b => b.wallet_hash === walletHash) || null; }; const handleBanWallet = async () => { if (!banModalWallet) return; setIsSubmittingBan(true); try { const res = await fetch(`${API_BASE_URL}/api/moderation/ban`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wallet_hash: banModalWallet, reason: banReason, banned_by: 'Admin Dashboard' }) }); if (res.ok) { setBanModalWallet(null); await fetchBannedWallets(); await fetchLogs(); } else { alert("Erreur lors du bannissement."); } } catch (e) { console.error("Erreur ban:", e); } finally { setIsSubmittingBan(false); } }; const handleUnbanWallet = async (walletHash: string) => { if (!confirm(`Voulez-vous vraiment débannir le wallet ${walletHash.substring(0, 10)}... ?`)) return; try { const res = await fetch(`${API_BASE_URL}/api/moderation/ban/${walletHash}`, { method: 'DELETE' }); if (res.ok) { await fetchBannedWallets(); await fetchLogs(); } } catch (e) { console.error("Erreur unban:", e); } }; const handleDeleteLog = async (id: number) => { if (!confirm("Voulez-vous vraiment supprimer ce log ?")) return; try { const res = await fetch(`${API_BASE_URL}/api/logs/${id}`, { method: 'DELETE' }); if (res.ok) { setLogs(prev => prev.filter(l => l.id !== id)); fetchLogs(); } } catch (e) { console.error("Erreur suppression:", e); } }; const handleClearAll = async () => { try { const res = await fetch(`${API_BASE_URL}/api/logs`, { method: 'DELETE' }); if (res.ok) { setLogs([]); setShowClearConfirm(false); fetchLogs(); } } catch (e) { console.error("Erreur vidage logs:", e); } }; const formatFileSize = (bytes: number | null) => { if (!bytes || bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; const filteredLogs = useMemo(() => { return logs.filter(log => { // Filter status if (statusFilter === "SUCCESS" && log.status !== "SUCCESS") return false; if (statusFilter === "FAILED" && log.status !== "FAILED") return false; if (statusFilter === "INVALID_TARGET" && log.target_status === "VALID") return false; if (statusFilter === "BANNED" && !isWalletBanned(log.wallet_hash)) return false; // Filter search if (!searchTerm.trim()) return true; const term = searchTerm.toLowerCase(); return ( (log.wallet_hash && log.wallet_hash.toLowerCase().includes(term)) || (log.session_id && log.session_id.toLowerCase().includes(term)) || (log.image_filename && log.image_filename.toLowerCase().includes(term)) || (log.device_model && log.device_model.toLowerCase().includes(term)) || (log.weapon && log.weapon.toLowerCase().includes(term)) || (log.target_type && log.target_type.toLowerCase().includes(term)) || (log.target_status && log.target_status.toLowerCase().includes(term)) || (log.ip_address && log.ip_address.toLowerCase().includes(term)) ); }); }, [logs, searchTerm, statusFilter, bannedWallets]); return (
{/* Header */}

Journal & Contrôle des Uploads

Historique des transferts, diagnostic OpenCV (détection cibles) et modération des wallets.

Exporter CSV {!showClearConfirm ? ( ) : (
Effacer tout ?
)}
{/* KPI Stats Cards */}

Total Uploads

{stats?.total_uploads ?? logs.length}

{stats && stats.total_uploads > 0 && ( {Math.round(((stats.success_count || 0) / stats.total_uploads) * 100)}% succès )}

Aujourd'hui

{stats?.today_uploads ?? 0}

Wallets Bannis

{bannedWallets.length}

/ {stats?.unique_wallets ?? 0} total

Volume Transféré

{formatFileSize(stats?.total_bytes || logs.reduce((acc, l) => acc + (l.file_size || 0), 0))}

{/* Filters & Search Toolbar */}
{/* Search */}
setSearchTerm(e.target.value)} placeholder="Rechercher par Wallet, Session, Cible, Image..." className="w-full bg-slate-950 border border-slate-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-xl pl-10 pr-4 py-2 text-sm text-slate-200 placeholder-slate-500 outline-none transition-all" /> {searchTerm && ( )}
{/* Status & OpenCV Filter */}
Filtre:
{/* Logs Table */}
{filteredLogs.map((log) => { const dateObj = new Date(log.timestamp); const isValidDate = !isNaN(dateObj.getTime()); const formattedDate = isValidDate ? dateObj.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" }) : log.timestamp; const formattedTime = isValidDate ? dateObj.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit", second: "2-digit" }) : ""; const banned = isWalletBanned(log.wallet_hash); return ( {/* Timestamp */} {/* OpenCV Diagnosis & Upload Status */} {/* Wallet Hash & Ban Badge */} {/* Session & Target */} {/* Device & IP */} {/* File & Size */} {/* Actions */} ); })}
Date & Heure Diagnostic OpenCV Wallet Contributeur Session / Cible Appareil Fichier Actions
{formattedDate} {formattedTime}
{log.status === "FAILED" ? ( UPLOAD ÉCHEC ) : log.target_status === "VALID" ? ( CIBLE CERTIFIÉE ({Math.round((log.target_confidence || 1) * 100)}%) ) : log.target_status === "SUSPICIOUS" ? ( DOUTEUSE (1 ANNEAU) ) : ( NON RECONNUE )} {log.target_rings_count ? ( {log.target_rings_count} anneaux concentriques ) : null}
{log.wallet_hash ? (
setSearchTerm(log.wallet_hash || "")} className="font-mono text-xs text-indigo-300 hover:text-indigo-200 bg-indigo-500/5 px-2 py-1 rounded border border-indigo-500/10 hover:border-indigo-500/30 cursor-pointer transition-colors" title="Cliquer pour filtrer par ce wallet" > {log.wallet_hash.length > 16 ? `${log.wallet_hash.substring(0, 8)}...${log.wallet_hash.substring(log.wallet_hash.length - 8)}` : log.wallet_hash}
{banned && ( WALLET BANNI )}
) : ( Anonyme / Non fourni )}
{log.session_id && (
ID: {log.session_id}
)}
{log.target_type && ( {log.target_type} )} {log.weapon && ( {log.weapon} )} {log.impacts_count > 0 && ( {log.impacts_count} imp. )}
{log.device_model || log.device_os || "Inconnu"}
{log.ip_address && ( {log.ip_address.replace("::ffff:", "")} )}
{log.image_filename ? ( {log.image_filename} ) : ( Aucun fichier )} {formatFileSize(log.file_size)}
{/* Ban / Unban Button */} {log.wallet_hash && ( banned ? ( ) : ( ) )}
{/* Empty state */} {filteredLogs.length === 0 && (

Aucun log trouvé

{searchTerm || statusFilter !== "ALL" ? "Aucun enregistrement ne correspond à vos critères de recherche." : "Les logs apparaîtront ici dès que des photos ou des sessions seront uploadées."}

)}
{/* Details Modal */} {selectedLog && (
{/* Modal Header */}

Détails du Log #{selectedLog.id}

{new Date(selectedLog.timestamp).toLocaleString()}

{/* Modal Content */}
{/* OpenCV Diagnostic Banner */}
{selectedLog.target_status === "VALID" ? : } Diagnostic OpenCV : {selectedLog.target_status || "VALID"}
Confiance : {Math.round((selectedLog.target_confidence || 1) * 100)}%

{selectedLog.target_details || "Cible détectée avec succès."}

{/* General Grid */}
Wallet Hash
{selectedLog.wallet_hash || "Non spécifié"}
{isWalletBanned(selectedLog.wallet_hash) && ( Banni ({getBannedInfo(selectedLog.wallet_hash)?.reason}) )}
Session ID
{selectedLog.session_id || "N/A"}
Appareil
{selectedLog.device_model || "Inconnu"} ({selectedLog.device_os || "OS inconnu"})
Adresse IP
{selectedLog.ip_address || "Inconnue"}
Cible & Arme
Type: {selectedLog.target_type || "N/A"} | Arme: {selectedLog.weapon || "N/A"} | {selectedLog.distance_meters ? `${selectedLog.distance_meters}m` : ""}
Fichiers & Taille
Image: {selectedLog.image_filename || "N/A"} ({formatFileSize(selectedLog.file_size)})
{/* Raw Metadata JSON */} {selectedLog.raw_metadata && (
Données Brutes (JSON)
                    {(() => {
                      try {
                        return JSON.stringify(JSON.parse(selectedLog.raw_metadata), null, 2);
                      } catch {
                        return selectedLog.raw_metadata;
                      }
                    })()}
                  
)}
{/* Modal Footer */}
{selectedLog.image_filename && ( Ouvrir la session )} {selectedLog.wallet_hash && ( isWalletBanned(selectedLog.wallet_hash) ? ( ) : ( ) )}
)} {/* Ban Confirmation Modal */} {banModalWallet && (

Bannir un Wallet

Bloquer définitivement tout upload futur de ce compte.

{banModalWallet}
)}
); }