/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Interface Administrateur - Onglets Dashboard et Système/Paramètres * ========================================================================= */ const { useState, useEffect, useRef } = React; const { Activity, Users, Building, Eye, MessageSquare, Terminal, Archive, Save, Download, RefreshCw, Trash2, Loader, ShieldAlert, CreditCard, FileText, AlertTriangle, Settings, Zap } = window; const decodeHTML = (html) => { const txt = document.createElement("textarea"); txt.innerHTML = html; return txt.value; }; // ========================================================================= // 1. ONGLET : TABLEAU DE BORD (SUPERVISION ET DIAGNOSTICS) // ========================================================================= window.AdminDashboardTab = ({ data, refreshData, showToast, setActiveTab }) => { const [isSaving, setIsSaving] = useState(false); const [diagnostics, setDiagnostics] = useState(null); const [logs, setLogs] = useState(''); const [backups, setBackups] = useState([]); const [isLoadingBackups, setIsLoadingBackups] = useState(false); const [showAllBackups, setShowAllBackups] = useState(false); const [sysSummary, setSysSummary] = useState({ loaded: false, diagStatus: 'emerald', backupStatus: 'emerald', logStatus: 'emerald', php: '', ram: '', db: '', disk: '', backupsCount: 0, logDesc: '', backupDesc: '' }); const [modalView, setModalView] = useState(null); const [pwdRequest, setPwdRequest] = useState(null); const [promptDialog, setPromptDialog] = useState(null); const fetchTelemetry = async (silent = false) => { try { const cacheBuster = '&_t=' + Date.now(); const fetchOpts = silent ? { cache: 'no-store', headers: { 'x-silent': '1' } } : { cache: 'no-store' }; const [diagRes, backRes, logRes] = await Promise.all([ fetch(window.ECO_CONFIG.apiBaseUrl + 'system/diagnostics' + cacheBuster + (silent ? '&silent=1' : ''), fetchOpts).then(r => r.json()), fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups' + cacheBuster + (silent ? '&silent=1' : ''), fetchOpts).then(r => r.json()), fetch(window.ECO_CONFIG.apiBaseUrl + 'system/logs' + cacheBuster + (silent ? '&silent=1' : ''), fetchOpts).then(r => r.json()) ]); if (diagRes.status === 'success') { const dbDetail = diagRes.data.find(d => d.id === 'db')?.detail || '0 ms'; const diskDetail = diagRes.data.find(d => d.id === 'disk')?.detail || '0 GB'; const dbVal = parseFloat(dbDetail); const diskVal = parseFloat(diskDetail); let diagStatus = 'emerald'; if (dbVal > 500 || diskVal < 1) diagStatus = 'red'; else if (dbVal > 200 || diskVal < 5) diagStatus = 'amber'; const backupsData = backRes.data || []; const backupsCount = backupsData.length; let backupStatus = 'emerald'; let backupDesc = `${backupsCount} archive(s)`; if (backupsCount === 0) { backupStatus = 'red'; backupDesc = "Alerte : Aucune sauvegarde"; } else { const latestBackup = backupsData.reduce((max, b) => b.date > max.date ? b : max, backupsData[0]); const hoursSinceLast = (Date.now() - latestBackup.date) / (1000 * 60 * 60); if (hoursSinceLast > 48) { backupStatus = 'red'; backupDesc = `Critique : Retard de ${Math.floor(hoursSinceLast/24)} jours`; } else if (hoursSinceLast > 24) { backupStatus = 'amber'; backupDesc = `Attention : Retard de ${Math.floor(hoursSinceLast)}h`; } else { backupStatus = 'emerald'; backupDesc = `À jour (${backupsCount} archives)`; } } const logsContent = logRes.data?.logs || ''; let logStatus = 'emerald'; let logDesc = 'Journal vierge (Sain)'; const lowerLogs = logsContent.toLowerCase(); if (lowerLogs.includes('fatal error') || lowerLogs.includes('exception')) { logStatus = 'red'; logDesc = 'Erreurs critiques détectées !'; } else if (logsContent.length > 5) { logStatus = 'amber'; logDesc = 'Avertissements enregistrés'; } setSysSummary({ loaded: true, diagStatus, backupStatus, logStatus, php: diagRes.data.find(d => d.id === 'php')?.detail || 'OK', ram: diagRes.data.find(d => d.id === 'ram')?.detail || 'OK', db: dbDetail, disk: diskDetail, backupsCount, logDesc, backupDesc }); } } catch(e) {} }; useEffect(() => { fetchTelemetry(); let telemetryInterval; let currentDelay = 10000; let lastActivityTime = Date.now(); let isTabVisible = !document.hidden; const performTelemetrySync = async () => { if (!isTabVisible) return; await fetchTelemetry(true); const idleTime = Date.now() - lastActivityTime; if (idleTime > 120000) currentDelay = 60000; else if (idleTime > 30000) currentDelay = 30000; else currentDelay = 10000; scheduleNextPoll(currentDelay); }; const scheduleNextPoll = (delay) => { clearTimeout(telemetryInterval); if (isTabVisible) telemetryInterval = setTimeout(performTelemetrySync, delay); }; const handleVisibilityChange = () => { isTabVisible = !document.hidden; if (isTabVisible) { lastActivityTime = Date.now(); currentDelay = 10000; performTelemetrySync(); } else { clearTimeout(telemetryInterval); } }; const handleUserInteraction = () => { if (currentDelay > 10000) { lastActivityTime = Date.now(); currentDelay = 10000; scheduleNextPoll(10000); } else { lastActivityTime = Date.now(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('click', handleUserInteraction); document.addEventListener('keydown', handleUserInteraction); scheduleNextPoll(currentDelay); return () => { clearTimeout(telemetryInterval); document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('click', handleUserInteraction); document.removeEventListener('keydown', handleUserInteraction); }; }, []); useEffect(() => { let interval; if (modalView === 'diagnostics') { interval = setInterval(() => { fetch(window.ECO_CONFIG.apiBaseUrl + 'system/diagnostics&silent=1&_t=' + Date.now(), { cache: 'no-store', headers: { 'x-silent': '1' } }) .then(r => r.json()) .then(d => { if (d.status === 'success') setDiagnostics(d.data); }).catch(()=>{}); }, 1000); } return () => clearInterval(interval); }, [modalView]); const fetchDiagnostics = async () => { setModalView('diagnostics'); setDiagnostics(null); const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/diagnostics&_t=' + Date.now(), { cache: 'no-store' }); const d = await res.json(); if(d.status === 'success') setDiagnostics(d.data); }; const fetchLogs = async () => { setModalView('logs'); setLogs('Chargement...'); const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/logs&_t=' + Date.now(), { cache: 'no-store' }); const d = await res.json(); setLogs(d.data?.logs || 'Aucun log trouvé.'); }; const clearLogs = async () => { await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/logs/clear', { method: 'POST' }); fetchLogs(); fetchTelemetry(); }; const loadBackups = async () => { setModalView('backups'); setShowAllBackups(false); setIsLoadingBackups(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups&_t=' + Date.now(), { cache: 'no-store' }); const d = await res.json(); if(d.status === 'success') setBackups(d.data); } catch(e) { showToast("Erreur lors du chargement des sauvegardes", "error"); } finally { setIsLoadingBackups(false); } }; const createBackup = () => { setPromptDialog({ title: "Nouvelle sauvegarde", message: "Saisissez un marqueur ou label personnalisé pour identifier cette sauvegarde manuelle :", placeholder: "Ex : Avant la mise à jour...", confirmText: "Sauvegarder", onConfirm: async (label) => { setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups/create', { method: 'POST', body: JSON.stringify({ label }) }); if ((await res.json()).status === 'success') { showToast("Sauvegarde créée avec succès.", "success"); loadBackups(); fetchTelemetry(); } else showToast("Erreur de sauvegarde", "error"); } catch(e) { showToast("Erreur réseau", "error"); } finally { setIsSaving(false); } } }); }; const restoreBackup = (filename) => { setPwdRequest({ title: "Restauration système", desc: "ATTENTION CRITIQUE : La restauration écrasera la base de données actuelle. Saisissez le mot de passe administrateur :", onConfirm: async (pwd) => { setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups/restore', { method: 'POST', body: JSON.stringify({ filename, pwd }) }); if ((await res.json()).status === 'success') { alert("Restauration réussie. L'application va recharger."); window.location.reload(); } else showToast("Mot de passe incorrect ou erreur.", "error"); } finally { setIsSaving(false); } } }); }; const deleteBackup = (filename) => { setPwdRequest({ title: "Suppression d'archive", desc: "Saisissez le mot de passe administrateur pour supprimer définitivement cette sauvegarde :", onConfirm: async (pwd) => { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups/delete', { method: 'POST', body: JSON.stringify({ filename, pwd }) }); if ((await res.json()).status === 'success') { loadBackups(); fetchTelemetry(); } else { showToast("Mot de passe incorrect", "error"); } } }); }; const getColorClass = (status) => { if (!sysSummary.loaded) return 'text-slate-500 border-slate-200 bg-slate-50'; if (status === 'red') return 'text-red-700 border-red-200 bg-red-50 hover:border-red-400'; if (status === 'amber') return 'text-amber-700 border-amber-200 bg-amber-50 hover:border-amber-400'; return 'text-emerald-700 border-emerald-200 bg-emerald-50 hover:border-emerald-400'; }; const clients = data.clients || []; const panneaux = data.panneaux || []; const activePanneaux = panneaux.filter(p => p.status === 'Actif' && p.id !== 'demo-panneau'); const mrr = activePanneaux.filter(p => p.offerType === 'rental').reduce((sum, p) => sum + (p.currentRate || 0), 0); const interactions = data.interactions || []; const recentMessages = [...interactions].filter(m => m.target === 'Admin').reverse().slice(0, 5); return (
Vue d'ensemble de la plateforme SaaS.
{displayAuthor}
{m.detail.replace(/\[ATTACHMENT:[^\]]+\]/g, '[Pièce jointe]').replace(/<[^>]*>?/gm, '')}
{m.panneauId === 'CONTACT_PUBLIC' ? 'ORIGINE : Nous contacter' : 'ORIGINE : Espace client (Support)'}
Aucune interaction récente.
}Analyse complète en cours...
{d.label}
{d.status}
{logs}
Chargement en cours...
{decodeHTML(b.label)}
{new Date(b.date).toLocaleString('fr-FR')} • {b.size}
Aucune sauvegarde trouvée.
} {!showAllBackups && backups.length > 5 && ( )} > )}Tarification globale et mentions légales.
Paramétrez les seuils de déclenchement du confinement automatique en cas d'attaque réseau.
Nombre d'erreurs de mot de passe avant bannissement de l'IP (15 min).
setSettingsData({...settingsData, sec_ip_limit: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-2 text-sm focus:border-red-500 outline-none transition" />Nombre d'IPs malveillantes simultanées avant confinement du site.
setSettingsData({...settingsData, sec_global_limit: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-2 text-sm focus:border-red-500 outline-none transition" />En minutes (Valeur aléatoire entre le min et le max).
setSettingsData({...settingsData, sec_lock_min: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-2 text-sm focus:border-red-500 outline-none transition" />En minutes.
setSettingsData({...settingsData, sec_lock_max: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-2 text-sm focus:border-red-500 outline-none transition" />Affichez des messages importantes aux utilisateurs de la plateforme.
Visible par les clients et visiteurs (sauf espace riverain).
Visible uniquement par les riverains qui scannent un panneau.
Ces listes permettent de détecter les panneaux ou messages ne respectant pas les règles de bienséance. (Mots séparés par des virgules)
Sélectionnez les fonctionnalités accessibles aux clients utilisant l'interface "Simplifiée".
En désactivant ce champ, vous indiquez que votre propre société (la plateforme eco-panneau) n'est pas assujettie à la TVA (ex : auto-entrepreneur).
Conséquence immédiate :