// ECO-PANNEAU.FR - _react/admin/_admin_modals_backups.jsx window.pano_AdminModalsBackups = ({ activeModal, activeDialog, closeCurrentLayer, openLocalDialog, fetchTelemetry, showToast }) => { const { useState, useEffect } = React; // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); const [isSaving, setIsSaving] = useState(false); const [isLoadingBackups, setIsLoadingBackups] = useState(true); const [backupsList, setBackupsList] = useState([]); const [backupDesc, setBackupDesc] = useState(''); const [showAllBackups, setShowAllBackups] = useState(false); const [pendingBackup, setPendingBackup] = useState(null); const [backupToRestore, setBackupToRestore] = useState(null); const [restorePwd, setRestorePwd] = useState(''); const [confirmConfig, setConfirmConfig] = useState(null); // 1. - Composants et Icônes const { Modal, ConfirmModal, Button, FormInput } = window.pano_getComponents(); const { ArchiveIcon, Trash2Icon, RefreshCwIcon, DownloadIcon, CheckCircleIcon, ShieldIcon, LoaderIcon } = window.pano_getIcons(); // 2. - Chargement initial useEffect(() => { if (activeModal === 'backups') { loadBackups(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeModal]); const loadBackups = async () => { setIsLoadingBackups(true); try { const d = await safeFetch(`system/backups&_t=${Date.now()}`, { method: 'GET', silent: true }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d && d.status === 'success') { setBackupsList(d.data || []); setShowAllBackups(false); } } catch (e) { if (!isMounted.current) return; // SÉCURITÉ if (window.pano_logFallback) window.pano_logFallback(`Erreur de chargement des backups: ${e.message}`); } if (isMounted.current) setIsLoadingBackups(false); }; // 3. - Polling de génération d'archive (Conserve Fetch natif car c'est une boucle autonome) useEffect(() => { let interval; if (pendingBackup) { interval = setInterval(async () => { try { // CORRECTION : Ajout du header x-silent pour rendre le polling furtif const res = await fetch(`${window.pano_CONFIG.apiBaseUrl}system/backups&_t=${Date.now()}`, { headers: { 'x-silent': '1' } }); const d = await res.json(); if (!isMounted.current) { clearInterval(interval); return; } // SÉCURITÉ : Arrêt du polling si démonté if (d && d.status === 'success') { setBackupsList(d.data || []); const found = (d.data || []).find(b => b.name === pendingBackup); if (found) { setPendingBackup(null); clearInterval(interval); if (showToast) showToast("L'archive a été générée avec succès !", "success"); if (fetchTelemetry) fetchTelemetry(); } } } catch(e) {} }, 3000); } return () => clearInterval(interval); }, [pendingBackup, fetchTelemetry, showToast]); const manualBackupsCount = backupsList.filter(b => b.label !== 'Automatique').length; const sortedBackups = [...backupsList].sort((a, b) => b.date - a.date); const displayedBackups = showAllBackups ? sortedBackups : sortedBackups.slice(0, 5); // 4. - Création de sauvegarde const handleCreateBackup = async () => { const d = await safeFetch('system/backups/create', { body: { label: backupDesc.trim() }, setLoading: setIsSaving }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d && d.data && d.data.filename) { setBackupDesc(''); closeCurrentLayer(); setPendingBackup(d.data.filename); if (showToast) showToast("La création de l'archive a débuté en arrière-plan...", "info"); } }; // 5. - Restauration const confirmRestore = (b) => { setBackupToRestore(b); openLocalDialog('restore_prompt'); }; const executeRestore = async () => { if (!restorePwd) { if (showToast) showToast("Mot de passe requis.", "error"); return; } closeCurrentLayer(); const d = await safeFetch('system/backups/restore', { body: { filename: backupToRestore.name, pwd: restorePwd }, setLoading: setIsSaving, successMessage: "Restauration terminée. Le système a été réinitialisé." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d) { setRestorePwd(''); setBackupToRestore(null); setTimeout(() => window.location.reload(), 1500); } }; // 6. - Suppression et nettoyage const handleDeleteBackup = (b) => { setConfirmConfig({ title: "Supprimer la sauvegarde", message: `Voulez-vous vraiment supprimer la sauvegarde "${b.label}" ?`, confirmText: "Oui, supprimer", isDestructive: true, onConfirm: async () => { setConfirmConfig(null); const d = await safeFetch('system/backups/delete', { body: { file: b.name }, setLoading: setIsSaving, successMessage: "Sauvegarde supprimée." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d) { loadBackups(); if (fetchTelemetry) fetchTelemetry(); } }, onCancel: () => setConfirmConfig(null) }); openLocalDialog('confirm'); }; const handleCleanupBackups = () => { setConfirmConfig({ title: "Nettoyage des archives", message: "Voulez-vous vraiment supprimer toutes les sauvegardes manuelles à l'exception des 5 plus récentes ? Les sauvegardes automatiques ne seront pas affectées.", confirmText: "Oui, nettoyer", isDestructive: true, onConfirm: async () => { setConfirmConfig(null); const d = await safeFetch('system/backups/cleanup', { method: 'POST', setLoading: setIsSaving, successMessage: "Archives manuelles obsolètes purgées avec succès." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d) { loadBackups(); if (fetchTelemetry) fetchTelemetry(); } }, onCancel: () => setConfirmConfig(null) }); openLocalDialog('confirm'); }; // 7. - Rendu UI return ( <> {activeModal === 'backups' && ( ( <> )} >

Archives de la plateforme

Sauvegardes automatisées et manuelles de la base de données (JSON) et des fichiers vitaux.

{isLoadingBackups ? (
Chargement des archives...
) : ( <>
{window.pano_formatPlural(sortedBackups.length, 'archive disponible', 'archives disponibles')}
{pendingBackup && (

Génération en cours...

Le fichier apparaîtra ici automatiquement dans quelques instants.

)} {displayedBackups.map((b, i) => { const isAuto = b.label === 'Automatique' || b.name.includes('Automatique'); const iconColorClass = isAuto ? 'bg-purple-100 text-purple-600' : 'bg-blue-100 text-blue-600'; return (

{b.label} {i === 0 && !showAllBackups && !pendingBackup && Actuelle}

{new Date(b.date).toLocaleString('fr-FR')} • {b.size}

{DownloadIcon && }
); })} {!showAllBackups && sortedBackups.length > 5 && (
)} {sortedBackups.length === 0 && !pendingBackup && (
Aucune sauvegarde trouvée sur le serveur.
)} )}
)} {activeDialog === 'backup_prompt' && ( ( <> )} >
{ e.preventDefault(); handleCreateBackup(); }} className="space-y-4 min-w-0">

Donnez une description courte à cette archive (ex: "Avant mise à jour V2"). Optionnel.

setBackupDesc(e.target.value)} disabled={isSaving} placeholder="Description de l'archive (Optionnel)..." className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition font-bold min-w-0" autoFocus />
)} {activeDialog === 'restore_prompt' && backupToRestore && ( ( <> )} >
{ e.preventDefault(); executeRestore(); }} className="space-y-4 min-w-0">

Vous êtes sur le point d'écraser la base de données actuelle avec l'archive du {new Date(backupToRestore.date).toLocaleString('fr-FR')}. Toutes les données générées depuis cette date seront définitivement perdues.

} type="password" value={restorePwd} onChange={e => setRestorePwd(e.target.value)} placeholder="Saisissez votre mot de passe principal..." required autoFocus className="min-w-0 w-full" />
)} {activeDialog === 'confirm' && confirmConfig && ConfirmModal && } ); }; /* EOF ========== [_react/admin/_admin_modals_backups.jsx] */