// ECO-PANNEAU.FR - _react/admin/_admin_modals_system.jsx window.pano_AdminModalsSystem = ({ activeModal, closeCurrentLayer, showToast, data, sysSummary = {}, refreshData, fetchTelemetry }) => { const { useState, useEffect } = React; const [isSaving, setIsSaving] = useState(false); const [isLoadingLogs, setIsLoadingLogs] = useState(false); // 1. - SÉCURITÉ ANTI-FUITE DE MÉMOIRE (Zéro-Dette) const { isMounted, safeFetch } = window.pano_useSafeFetch(); const { openModal, replaceCurrentLayer } = window.pano_useUrlModal ? window.pano_useUrlModal() : {}; // GESTION DU BOUTON RETOUR VERS LE CENTRE DE CONTRÔLE const handleBackToDiagnostics = () => { if (replaceCurrentLayer) replaceCurrentLayer('modal', 'diagnostics', null, false); else closeCurrentLayer(); }; // 2. - États const [codeStats, setCodeStats] = useState(null); const [statsMode, setStatsMode] = useState('source'); const [sortConfig, setSortConfig] = useState({ key: 'path', direction: 'asc' }); const [logTab, setLogTab] = useState('php'); const [logs, setLogs] = useState(''); const [diagnostics, setDiagnostics] = useState([]); const [juridiqueLogs, setJuridiqueLogs] = useState([]); const [juridiqueClientsMap, setJuridiqueClientsMap] = useState({}); const [hasMoreLogs, setHasMoreLogs] = useState(false); const [jFilterClient, setJFilterClient] = useState(''); const [jFilterKeyword, setJFilterKeyword] = useState(''); // Zéro-Trust : Stockage temporaire de la clé confirm const [aesKeyConfirm, setAesKeyConfirm] = useState(''); const [isAesKeyVerified, setIsAesKeyVerified] = useState(false); // 3. - Composants et Icônes const { Modal, Button, CardGrid, DataCard, IconBadge } = window.pano_getComponents(); const { ActivityIcon, TerminalIcon, FileDigitIcon, Trash2Icon, CopyIcon, CheckCircleIcon, ShieldCheckIcon, SearchIcon, DownloadIcon, UserIcon, LoaderIcon, FileTextIcon, LockIcon, ArchiveIcon, RefreshCwIcon, ServerIcon } = window.pano_getIcons(); // 4. - Chargement dynamique des données useEffect(() => { if (activeModal === 'code_stats') loadCodeStats(); if (activeModal === 'logs') { if (logTab === 'php') loadLogs(); if (logTab === 'juridique' && isAesKeyVerified) loadJuridiqueLogs(); } if (activeModal === 'diagnostics') { loadDiagnostics(false); // Chargement initial // SUPERVISION EN TEMPS RÉEL (Auto-Refresh silencieux toutes les 10s) const diagInterval = setInterval(() => { if (isMounted.current) loadDiagnostics(true); }, 10000); return () => clearInterval(diagInterval); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeModal, statsMode, logTab, isAesKeyVerified]); const loadCodeStats = async () => { setCodeStats(null); const d = await safeFetch(`system/code_stats&filter=${statsMode}`, { method: 'GET' }); if (!isMounted.current) return; // Sécurité if (d && d.status === 'success') setCodeStats(d.data); }; const loadLogs = async () => { const d = await safeFetch('system/logs', { method: 'GET' }); if (!isMounted.current) return; // Sécurité if (d && d.status === 'success') setLogs(d.data?.logs || ''); }; const loadJuridiqueLogs = async (overrideClient = null) => { if (!isAesKeyVerified || !aesKeyConfirm) return; setIsLoadingLogs(true); const currentClient = overrideClient !== null ? overrideClient : jFilterClient; const payload = { aes_key_confirm: aesKeyConfirm, client: currentClient, keyword: jFilterKeyword }; const d = await safeFetch('system/juridique_logs', { method: 'POST', body: payload }); if (!isMounted.current) return; // Sécurité if (d && d.status === 'success') { setJuridiqueLogs(d.data.logs || []); setJuridiqueClientsMap(d.data.clients || {}); setHasMoreLogs(d.data.has_more || false); } else { setIsAesKeyVerified(false); setAesKeyConfirm(''); } setIsLoadingLogs(false); }; const handleVerifyAesKey = async (e) => { e.preventDefault(); setIsLoadingLogs(true); const payload = { aes_key_confirm: aesKeyConfirm }; const d = await safeFetch('system/juridique_logs', { method: 'POST', body: payload }); if (!isMounted.current) return; // Sécurité if (d && d.status === 'success') { setIsAesKeyVerified(true); setJuridiqueLogs(d.data.logs || []); setJuridiqueClientsMap(d.data.clients || {}); setHasMoreLogs(d.data.has_more || false); if (window.pano_showToast) window.pano_showToast("Accès au registre déverrouillé.", "success"); } else { setAesKeyConfirm(''); } setIsLoadingLogs(false); }; const loadDiagnostics = async (isSilent = false) => { const options = { method: 'GET' }; if (isSilent) { options.headers = { 'x-silent': '1' }; // Empêche l'affichage du Loader global pour l'UX } // CORRECTION SÉCURITÉ & UX : Ajout d'un cache-buster pour empêcher le navigateur de figer la réponse const d = await safeFetch(`system/diagnostics&_t=${Date.now()}`, options); if (!isMounted.current) return; // Sécurité if (d && d.status === 'success') { const apiDiags = d.data || []; const adminSettings = data?.settings || {}; // CORRECTION: Validation souple des valeurs numériques ou string "0"/"1" const isBillingComplete = !!( adminSettings.billing_company?.trim() && adminSettings.billing_address?.trim() && adminSettings.billing_siret?.trim() && (adminSettings.billing_has_tva == 0 || adminSettings.billing_tva?.trim()) ); const isMaintenance = adminSettings.maintenance == 1; const purchasesAllowed = adminSettings.allow_new_purchases != 0; const localDiags = [ { name: "Mode maintenance", status: isMaintenance ? 'error' : 'success', detail: isMaintenance ? 'Site hors ligne' : 'Désactivé' }, { name: "Prise de commandes", status: purchasesAllowed ? 'success' : 'warning', detail: purchasesAllowed ? 'Autorisée' : 'Suspendue' }, { name: "Profil de facturation", status: isBillingComplete ? 'success' : 'error', detail: isBillingComplete ? 'Conforme' : 'Incomplet' } ]; setDiagnostics([...localDiags, ...apiDiags]); } }; // 5. - Actions métiers (Journaux et PDF) const handleClearLogs = async () => { const d = await safeFetch('system/logs/clear', { method: 'POST', setLoading: setIsSaving, successMessage: "Journaux purgés." }); if (!isMounted.current) return; // Sécurité if (d) setLogs(''); }; const handleCopyLogs = () => { navigator.clipboard.writeText(logs); if (window.pano_showToast) window.pano_showToast("Journaux copiés dans le presse-papiers.", "success"); }; const generateJuridiquePDF = async () => { setIsSaving(true); try { const jsPDF = window.jspdf ? window.jspdf.jsPDF : null; if (!jsPDF) throw new Error("Bibliothèque PDF introuvable."); const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' }); const loadFont = async (fontName, fontStyle, url) => { const cacheKey = `${fontName}-${fontStyle}`; let base64 = window.pano_CONFIG?.pdfFontsCache?.[cacheKey]; if (!base64) { const resp = await fetch(url, { headers: { 'x-silent': '1' } }); const buffer = await resp.arrayBuffer(); let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); base64 = window.btoa(binary); if (!window.pano_CONFIG) window.pano_CONFIG = {}; if (!window.pano_CONFIG.pdfFontsCache) window.pano_CONFIG.pdfFontsCache = {}; window.pano_CONFIG.pdfFontsCache[cacheKey] = base64; } const fileName = `${cacheKey}.ttf`; doc.addFileToVFS(fileName, base64); doc.addFont(fileName, fontName, fontStyle); }; const assetsUrl = (window.pano_CONFIG && window.pano_CONFIG.assetsUrl) ? window.pano_CONFIG.assetsUrl : '_assets/'; await Promise.all([ loadFont("Roboto", "normal", window.pano_CONFIG?.fonts?.robotoRegular || (assetsUrl + "roboto/Roboto-Regular.ttf")), loadFont("Roboto", "bold", window.pano_CONFIG?.fonts?.robotoMedium || (assetsUrl + "roboto/Roboto-Medium.ttf")) ]); if (!isMounted.current) return; const mainFont = "Roboto"; const splitLongText = (text, maxWidth) => { const initialLines = doc.splitTextToSize(text, maxWidth); const result = []; initialLines.forEach(line => { if (doc.getTextWidth(line) > maxWidth) { let currentLine = ''; for (let i = 0; i < line.length; i++) { if (doc.getTextWidth(currentLine + line[i]) > maxWidth && currentLine.length > 0) { result.push(currentLine); currentLine = line[i]; } else { currentLine += line[i]; } } if (currentLine) result.push(currentLine); } else { result.push(line); } }); return result; }; let y = 10; const addNewPage = () => { doc.addPage(); y = 10; doc.setFont(mainFont, "bold"); doc.setFontSize(10); doc.setTextColor(255, 255, 255); doc.setFillColor(15, 23, 42); doc.rect(10, y, 277, 8, 'F'); doc.text("Date", 12, y + 5.5); doc.text("Type", 35, y + 5.5); doc.text("Client concerné", 60, y + 5.5); doc.text("Détails / Actions", 110, y + 5.5); y += 12; }; doc.setFont(mainFont, "bold"); doc.setFontSize(18); doc.setTextColor(15, 23, 42); doc.text("Registre d'audit eco-panneau.fr", 10, y + 5); y += 12; doc.setFontSize(10); doc.setFont(mainFont, "normal"); doc.setTextColor(71, 85, 105); const generationDateStr = new Date().toLocaleString('fr-FR'); doc.text(`Document généré le : ${generationDateStr}`, 10, y); y += 6; if (jFilterClient) { const cInfo = juridiqueClientsMap[jFilterClient]; const cTitle = cInfo ? `${cInfo.name} ${cInfo.email ? `(${cInfo.email})` : ''} ${!cInfo.active ? '(Compte supprimé)' : ''}` : jFilterClient; doc.text(`Filtre Client : ${cTitle}`, 10, y); y += 6; } if (jFilterKeyword) { doc.text(`Filtre Mots-clés : "${jFilterKeyword}"`, 10, y); y += 6; } y += 4; doc.setFont(mainFont, "bold"); doc.setTextColor(255, 255, 255); doc.setFillColor(15, 23, 42); doc.rect(10, y, 277, 8, 'F'); doc.text("Date", 12, y + 5.5); doc.text("Type", 35, y + 5.5); doc.text("Client concerné", 60, y + 5.5); doc.text("Détails / Actions", 110, y + 5.5); y += 12; if (juridiqueLogs.length === 0) { doc.setTextColor(100, 116, 139); doc.setFont(mainFont, "normal"); doc.text("Aucun enregistrement trouvé pour ces critères.", 10, y + 5); } else { for (let i = 0; i < juridiqueLogs.length; i++) { const log = juridiqueLogs[i]; const cInfo = log.client_uid ? juridiqueClientsMap[log.client_uid] : null; let clientText = 'Système'; if (cInfo) { const parts = []; if (cInfo.name) parts.push(cInfo.name); if (cInfo.full_name) parts.push(cInfo.full_name); if (cInfo.email) parts.push(cInfo.email); clientText = parts.join('\n'); if (!cInfo.active) clientText += '\n[Supprimé]'; } const cleanContent = (log.content || '').replace(/[\u1000-\uFFFF]/g, ''); const contentLines = splitLongText(cleanContent, 172); const clientLines = splitLongText(clientText, 45); const typeLines = splitLongText(log.type || 'Autre', 23); const maxLines = Math.max(2, typeLines.length, contentLines.length, clientLines.length); const rowHeight = Math.max(8, maxLines * 4.0); if (y + rowHeight > 195) { addNewPage(); } doc.setTextColor(15, 23, 42); doc.setFontSize(9); doc.setFont(mainFont, "normal"); const dateStr = new Date(log.date).toLocaleString('fr-FR'); const [datePart, timePart] = dateStr.split(' '); doc.text(datePart, 12, y + 3); if (timePart) doc.text(timePart, 12, y + 7); doc.setFont(mainFont, "bold"); doc.text(typeLines, 35, y + 3); doc.setFont(mainFont, "normal"); if (cInfo && !cInfo.active) doc.setTextColor(220, 38, 38); else doc.setTextColor(15, 23, 42); doc.text(clientLines, 60, y + 3); doc.setTextColor(71, 85, 105); doc.text(contentLines, 110, y + 3); y += rowHeight + 2; doc.setDrawColor(226, 232, 240); doc.line(10, y, 287, y); y += 2; } } const pageCount = doc.internal.getNumberOfPages(); doc.setFont(mainFont, "normal"); doc.setFontSize(7); doc.setTextColor(148, 163, 184); const generatedText = `Document généré le : ${generationDateStr}`; for (let i = 1; i <= pageCount; i++) { doc.setPage(i); if (i > 1) { doc.text(generatedText, 6, 204); } doc.text(`page ${i} sur ${pageCount}`, 291, 204, { align: 'right' }); } doc.save(`eco-panneau_registre_audit_${Date.now()}.pdf`); if (window.pano_showToast) window.pano_showToast("Rapport PDF généré avec succès.", "success"); } catch (err) { if (window.pano_showToast) window.pano_showToast("Erreur lors de la génération du PDF : " + err.message, "error"); } finally { if (isMounted.current) setIsSaving(false); } }; // 6. - Tri de l'arborescence (Mode Risque) const getRiskScore = (f) => { if (f.valid === false && !f.protected) return 3; if (f.valid === false && f.protected) return 2; return 1; }; const sortedFiles = React.useMemo(() => { if (!codeStats || !codeStats.details) return []; let sortableItems = [...codeStats.details]; if (sortConfig !== null) { sortableItems.sort((a, b) => { let valA, valB; if (sortConfig.key === 'risk') { valA = getRiskScore(a); valB = getRiskScore(b); } else { valA = a[sortConfig.key] !== undefined ? a[sortConfig.key] : ''; valB = b[sortConfig.key] !== undefined ? b[sortConfig.key] : ''; if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); } if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1; if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1; const pathA = a.path ? a.path.toLowerCase() : ''; const pathB = b.path ? b.path.toLowerCase() : ''; if (pathA < pathB) return -1; if (pathA > pathB) return 1; return 0; }); } return sortableItems; }, [codeStats, sortConfig]); const requestSort = (key) => { let direction = (key === 'lines' || key === 'chars' || key === 'risk') ? 'desc' : 'asc'; if (sortConfig && sortConfig.key === key) { direction = sortConfig.direction === 'asc' ? 'desc' : 'asc'; } setSortConfig({ key, direction }); }; const renderSortButton = (key, label) => { const isActive = sortConfig?.key === key; const icon = isActive ? (sortConfig.direction === 'asc' ? '↓' : '↑') : ''; return ( ); }; // 7. - Rendu UI const getVariant = (status) => !sysSummary.loaded ? 'default' : (status === 'emerald' ? 'success' : (status === 'amber' ? 'warning' : 'danger')); const backupVariant = getVariant(sysSummary.backupStatus); const logVariant = getVariant(sysSummary.logStatus); return ( <> {activeModal === 'code_stats' && ( ( )} > {!codeStats ? (
Analyse de l'arborescence en cours...
) : (

{(codeStats.total_lines || 0).toLocaleString('fr-FR')}

Lignes analysées

{(codeStats.total_chars || 0).toLocaleString('fr-FR')}

Caractères

{(codeStats.total_files || 0).toLocaleString('fr-FR')}

Fichiers trouvés

{((codeStats.total_chars || 0) / 1024 / 1024).toFixed(2)} Mo

Poids du code lu

Trier l'arborescence :
{renderSortButton('path', 'Chemin')} {renderSortButton('risk', 'Risque')} {renderSortButton('lines', 'Lignes')} {renderSortButton('chars', 'Taille')}
{sortedFiles.map((f, i) => { const isError = f.valid === false && !f.protected; const isProtectedWrongPerms = f.valid === false && f.protected; return (
{f.type === 'dir' ? '📁' : '📄'}

{f.path}

{f.perms || '-'} {f.type === 'dir' && f.htaccess && ( {f.htaccess_deny ? '🔒 Deny' : '📄 Règles'} )} {(f.protected && !f.htaccess_deny) && ( 🔒 Protégé )}
{f.type !== 'dir' && f.lines > 0 && (
📝 {(f.lines || 0).toLocaleString('fr-FR')} 🔡 {(f.chars || 0).toLocaleString('fr-FR')}
)}
)})}
)}
)} {activeModal === 'logs' && ( ( )} > {logTab === 'php' ? (

Trace technique des erreurs et alertes serveur.

                                {logs || "Aucune erreur enregistrée dans le journal (fichier error_log vide)."}
                            
) : (
{!isAesKeyVerified ? (

Déverrouillage requis

L'accès au registre d'audit est protégé par chiffrement fort (Zero-Trust). Veuillez saisir votre clé de confirmation pour y accéder.

setAesKeyConfirm(e.target.value)} placeholder="AES_KEY_CONFIRM" className="w-full border border-slate-300 rounded-xl p-3 font-mono text-sm focus:border-blue-500 outline-none text-center" required />
) : ( <>
{ e.preventDefault(); loadJuridiqueLogs(); }} className="flex flex-col lg:flex-row gap-3 mb-4 bg-slate-50 p-3 rounded-xl border border-slate-200">
{SearchIcon && } setJFilterKeyword(e.target.value)} placeholder="Filtrer par mot-clé (ex: suppression)..." className="w-full border border-slate-300 rounded-lg p-2.5 pl-10 text-sm font-bold text-slate-700 focus:border-blue-500 outline-none" />
{window.pano_formatPlural(juridiqueLogs.length, 'résultat affiché', 'résultats affichés')}
{!hasMoreLogs && juridiqueLogs.length > 0 && ( )}
{isLoadingLogs ? (
) : null} {juridiqueLogs.length === 0 ? (
Aucun enregistrement trouvé pour ces critères.
) : ( <> {juridiqueLogs.map((log, i) => { const cInfo = log.client_uid ? juridiqueClientsMap[log.client_uid] : null; return (
{log.type || 'Autre'} {new Date(log.date).toLocaleString('fr-FR')}

{log.content}

{cInfo && (
{UserIcon && } Client : {cInfo.name} {cInfo.full_name ? `(${cInfo.full_name})` : ''} {!cInfo.active && [Supprimé]}
{cInfo.email &&
{cInfo.email}
}
)}
); })} {hasMoreLogs && (

L'affichage est limité aux 500 résultats les plus récents.

Pour consulter l'intégralité des enregistrements correspondant à votre recherche, veuillez télécharger le registre complet au format CSV.

)} )}
)}
)}
)} {activeModal === 'diagnostics' && ( ( )} >

État de santé

{diagnostics.length > 0 ? diagnostics.map((d, i) => { let currentStatus = d.status; let currentDetail = d.detail; let statusColor = 'text-emerald-500'; if (currentStatus === 'info') statusColor = 'text-blue-500'; if (currentStatus === 'warning') statusColor = 'text-amber-500'; if (currentStatus === 'error') statusColor = 'text-red-500'; return (

{d.name || d.label} {(currentStatus === 'success' || currentStatus === 'Opérationnel') && } {(currentStatus !== 'success' && currentStatus !== 'Opérationnel') && }

{currentDetail}

); }) : (
Récupération des diagnostics...
)}

Serveur et maintenance

{ replaceCurrentLayer('modal', 'backups', null, false); }} className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0">

Sauvegardes système

{sysSummary.loaded ? sysSummary.backupDesc : 'Vérification...'}

{ replaceCurrentLayer('modal', 'logs', null, false); }} className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0">

Journaux et Registre

{sysSummary.loaded ? sysSummary.logDesc : 'Chargement...'} (Erreurs et Légal)

{ replaceCurrentLayer('modal', 'code_stats', null, false); }} className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0">

Statistiques du code

Analyse des fichiers et droits

Moteur asynchrone (CRON)

Exécution manuelle forcée des tâches planifiées :

{ const d = await safeFetch('system/cron_cleanup&mode=light', { method: 'GET', setLoading: setIsSaving, successMessage: "CRON courant exécuté avec succès." }); if (!isMounted.current) return; if (d) { if (refreshData && fetchTelemetry) { refreshData(); fetchTelemetry(true); } loadDiagnostics(true); // Rafraîchissement direct avec Cache-Buster } }}>

CRON Courant (Rapide)

Traite les e-mails en attente, les notifications push et relance les invitations. (Exécution immédiate)

{ const d = await safeFetch('system/cron_cleanup&mode=full', { method: 'GET', setLoading: setIsSaving, successMessage: "CRON complet exécuté avec succès." }); if (!isMounted.current) return; // SÉCURITÉ if (d) { if (refreshData && fetchTelemetry) { refreshData(); fetchTelemetry(true); } loadDiagnostics(true); // Rafraîchissement direct avec Cache-Buster } }}>

CRON Complet (Lourd)

Version courante + Nettoyage de la BDD, purge des brouillons expirés, purges RGPD et sauvegarde globale.

)} ); }; /* EOF ========== [_react/admin/_admin_modals_system.jsx] */