feat: enhance backup management in AdvancedSettingsPage
- Added backup management functionality to `AdvancedSettingsPageClient`, including creating and verifying backups. - Updated `package.json` with new backup-related scripts. - Improved UI to display backup status and next scheduled backup time. - Updated `.gitignore` to exclude backup files. - Enhanced server-side data fetching to include backup data and database statistics.
This commit is contained in:
@@ -1,16 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { UserPreferences } from '@/lib/types';
|
||||
import { Header } from '@/components/ui/Header';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
|
||||
import { backupClient, BackupListResponse } from '@/clients/backup-client';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface DatabaseStats {
|
||||
taskCount: number;
|
||||
tagCount: number;
|
||||
completionRate: number;
|
||||
}
|
||||
|
||||
interface AdvancedSettingsPageClientProps {
|
||||
initialPreferences: UserPreferences;
|
||||
initialDbStats: DatabaseStats;
|
||||
initialBackupData: BackupListResponse;
|
||||
}
|
||||
|
||||
export function AdvancedSettingsPageClient({ initialPreferences }: AdvancedSettingsPageClientProps) {
|
||||
export function AdvancedSettingsPageClient({
|
||||
initialPreferences,
|
||||
initialDbStats,
|
||||
initialBackupData
|
||||
}: AdvancedSettingsPageClientProps) {
|
||||
const [backupData, setBackupData] = useState<BackupListResponse>(initialBackupData);
|
||||
const [dbStats] = useState<DatabaseStats>(initialDbStats);
|
||||
const [isCreatingBackup, setIsCreatingBackup] = useState(false);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
const reloadBackupData = async () => {
|
||||
try {
|
||||
const data = await backupClient.listBackups();
|
||||
setBackupData(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to reload backup data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
setIsCreatingBackup(true);
|
||||
try {
|
||||
await backupClient.createBackup();
|
||||
await reloadBackupData();
|
||||
} catch (error) {
|
||||
console.error('Failed to create backup:', error);
|
||||
} finally {
|
||||
setIsCreatingBackup(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyDatabase = async () => {
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
await backupClient.verifyDatabase();
|
||||
alert('✅ Base de données vérifiée avec succès');
|
||||
} catch (error) {
|
||||
console.error('Database verification failed:', error);
|
||||
alert('❌ Erreur lors de la vérification de la base');
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const formatTimeAgo = (date: Date): string => {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - new Date(date).getTime();
|
||||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `il y a ${diffMins}min`;
|
||||
} else if (diffHours < 24) {
|
||||
return `il y a ${diffHours}h ${diffMins % 60}min`;
|
||||
} else {
|
||||
return `il y a ${diffDays}j`;
|
||||
}
|
||||
};
|
||||
|
||||
const getNextBackupTime = (): string => {
|
||||
if (!backupData.scheduler.nextBackup) return 'Non planifiée';
|
||||
|
||||
const nextBackup = new Date(backupData.scheduler.nextBackup);
|
||||
const now = new Date();
|
||||
const diffMs = nextBackup.getTime() - now.getTime();
|
||||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `dans ${diffMins}min`;
|
||||
} else {
|
||||
return `dans ${diffHours}h ${diffMins % 60}min`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||
<div className="min-h-screen bg-[var(--background)]">
|
||||
@@ -52,33 +151,65 @@ export function AdvancedSettingsPageClient({ initialPreferences }: AdvancedSetti
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">Sauvegarde automatique</h3>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="font-medium">Sauvegarde automatique</h3>
|
||||
<span className={`h-2 w-2 rounded-full ${
|
||||
backupData.scheduler.isRunning ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}></span>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-2">
|
||||
Sauvegarde toutes les 6 heures (configurable)
|
||||
{backupData.scheduler.isEnabled
|
||||
? `Sauvegarde ${backupData.scheduler.interval === 'hourly' ? 'toutes les heures' :
|
||||
backupData.scheduler.interval === 'daily' ? 'quotidienne' : 'hebdomadaire'}`
|
||||
: 'Sauvegarde automatique désactivée'
|
||||
}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Prochaine sauvegarde: dans 3h 42min
|
||||
{backupData.scheduler.isRunning
|
||||
? `Prochaine sauvegarde: ${getNextBackupTime()}`
|
||||
: 'Planificateur arrêté'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">Sauvegardes disponibles</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-2">
|
||||
5 sauvegardes conservées
|
||||
</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Dernière: il y a 2h 18min
|
||||
{backupData.backups.length} sauvegarde{backupData.backups.length > 1 ? 's' : ''} conservée{backupData.backups.length > 1 ? 's' : ''}
|
||||
</p>
|
||||
{backupData.backups.length > 0 ? (
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Dernière: {formatTimeAgo(backupData.backups[0].createdAt)}
|
||||
({formatFileSize(backupData.backups[0].size)})
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Aucune sauvegarde disponible
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button className="px-3 py-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] rounded text-sm font-medium">
|
||||
Créer une sauvegarde
|
||||
</button>
|
||||
<button className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm font-medium">
|
||||
Gérer les sauvegardes
|
||||
</button>
|
||||
<Button
|
||||
onClick={handleCreateBackup}
|
||||
disabled={isCreatingBackup}
|
||||
className="px-3 py-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] rounded text-sm font-medium"
|
||||
>
|
||||
{isCreatingBackup ? 'Création...' : 'Créer une sauvegarde'}
|
||||
</Button>
|
||||
<Link href="/settings/backup">
|
||||
<Button className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm font-medium">
|
||||
Gérer les sauvegardes
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
onClick={handleVerifyDatabase}
|
||||
disabled={isVerifying}
|
||||
className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm font-medium"
|
||||
>
|
||||
{isVerifying ? 'Vérification...' : 'Vérifier DB'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -95,126 +226,31 @@ export function AdvancedSettingsPageClient({ initialPreferences }: AdvancedSetti
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-1">Tâches</h3>
|
||||
<p className="text-2xl font-bold text-[var(--primary)]">247</p>
|
||||
<p className="text-2xl font-bold text-[var(--primary)]">
|
||||
{dbStats.taskCount}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">entrées</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-1">Tags</h3>
|
||||
<p className="text-2xl font-bold text-[var(--primary)]">18</p>
|
||||
<p className="text-2xl font-bold text-[var(--primary)]">
|
||||
{dbStats.tagCount}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">entrées</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-1">Taille DB</h3>
|
||||
<p className="text-2xl font-bold text-[var(--primary)]">2.4</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">MB</p>
|
||||
<h3 className="font-medium mb-1">Taux complétion</h3>
|
||||
<p className="text-2xl font-bold text-[var(--primary)]">
|
||||
{dbStats.completionRate}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">%</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Export / Import */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold">📤 Export / Import</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Sauvegarde et restauration des données
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">Export des données</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Exporter toutes les données au format JSON
|
||||
</p>
|
||||
<button className="px-3 py-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] rounded text-sm">
|
||||
Télécharger export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">Import des données</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Restaurer des données depuis un fichier JSON
|
||||
</p>
|
||||
<button className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm">
|
||||
Choisir fichier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Logs et debug */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold">🐛 Debug et logs</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Outils de diagnostic et de résolution de problèmes
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">Logs système</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Consultation des logs d'erreur et d'activité
|
||||
</p>
|
||||
<button className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm">
|
||||
Voir les logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">Mode debug</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Activer les informations de debug détaillées
|
||||
</p>
|
||||
<button className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm">
|
||||
Activer debug
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Zone dangereuse */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold text-[var(--destructive)]">⚠️ Zone dangereuse</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Actions irréversibles - à utiliser avec précaution
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="p-4 bg-[var(--destructive)]/5 border border-[var(--destructive)]/20 rounded">
|
||||
<h3 className="font-medium mb-2 text-[var(--destructive)]">Réinitialisation complète</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
Supprime toutes les données (tâches, tags, préférences)
|
||||
</p>
|
||||
<button className="px-3 py-1.5 bg-[var(--destructive)] text-[var(--destructive-foreground)] rounded text-sm">
|
||||
Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Note développement futur */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="p-4 bg-[var(--warning)]/10 border border-[var(--warning)]/20 rounded">
|
||||
<p className="text-sm text-[var(--warning)] font-medium mb-2">
|
||||
🚧 Fonctionnalités en développement
|
||||
</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
La plupart des fonctions avancées seront implémentées dans les prochaines versions.
|
||||
Cette page sert de prévisualisation de l'interface à venir.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
585
components/settings/BackupSettingsPageClient.tsx
Normal file
585
components/settings/BackupSettingsPageClient.tsx
Normal file
@@ -0,0 +1,585 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { backupClient, BackupListResponse } from '@/clients/backup-client';
|
||||
import { BackupConfig } from '@/services/backup';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Header } from '@/components/ui/Header';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface BackupSettingsPageClientProps {
|
||||
initialData?: BackupListResponse;
|
||||
}
|
||||
|
||||
export default function BackupSettingsPageClient({ initialData }: BackupSettingsPageClientProps) {
|
||||
const [data, setData] = useState<BackupListResponse | null>(initialData || null);
|
||||
const [isLoading, setIsLoading] = useState(!initialData);
|
||||
const [isCreatingBackup, setIsCreatingBackup] = useState(false);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||
const [config, setConfig] = useState<BackupConfig | null>(initialData?.config || null);
|
||||
const [isSavingConfig, setIsSavingConfig] = useState(false);
|
||||
const [messages, setMessages] = useState<{[key: string]: {type: 'success' | 'error', text: string} | null}>({
|
||||
verify: null,
|
||||
config: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialData) {
|
||||
loadData();
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
// Helper pour définir un message contextuel
|
||||
const setMessage = (key: string, message: {type: 'success' | 'error', text: string} | null) => {
|
||||
setMessages(prev => ({ ...prev, [key]: message }));
|
||||
|
||||
// Auto-dismiss après 3 secondes
|
||||
if (message) {
|
||||
setTimeout(() => {
|
||||
setMessages(prev => ({ ...prev, [key]: null }));
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
console.log('🔄 Loading backup data...');
|
||||
const response = await backupClient.listBackups();
|
||||
console.log('✅ Backup data loaded:', response);
|
||||
setData(response);
|
||||
setConfig(response.config);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load backup data:', error);
|
||||
// Afficher l'erreur spécifique à l'utilisateur
|
||||
if (error instanceof Error) {
|
||||
console.error('Error details:', error.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
setIsCreatingBackup(true);
|
||||
try {
|
||||
await backupClient.createBackup();
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
console.error('Failed to create backup:', error);
|
||||
} finally {
|
||||
setIsCreatingBackup(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyDatabase = async () => {
|
||||
setIsVerifying(true);
|
||||
setMessage('verify', null);
|
||||
try {
|
||||
await backupClient.verifyDatabase();
|
||||
setMessage('verify', {type: 'success', text: 'Intégrité vérifiée'});
|
||||
} catch (error) {
|
||||
console.error('Database verification failed:', error);
|
||||
setMessage('verify', {type: 'error', text: 'Vérification échouée'});
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteBackup = async (filename: string) => {
|
||||
try {
|
||||
await backupClient.deleteBackup(filename);
|
||||
setShowDeleteConfirm(null);
|
||||
setMessage('restore', {type: 'success', text: `Sauvegarde ${filename} supprimée`});
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete backup:', error);
|
||||
setMessage('restore', {type: 'error', text: 'Suppression échouée'});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreBackup = async (filename: string) => {
|
||||
try {
|
||||
await backupClient.restoreBackup(filename);
|
||||
setShowRestoreConfirm(null);
|
||||
setMessage('restore', {type: 'success', text: 'Restauration réussie'});
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
console.error('Failed to restore backup:', error);
|
||||
setMessage('restore', {type: 'error', text: 'Restauration échouée'});
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleScheduler = async () => {
|
||||
if (!data) return;
|
||||
|
||||
try {
|
||||
await backupClient.toggleScheduler(!data.scheduler.isRunning);
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle scheduler:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!config) return;
|
||||
|
||||
setIsSavingConfig(true);
|
||||
setMessage('config', null);
|
||||
try {
|
||||
await backupClient.updateConfig(config);
|
||||
await loadData();
|
||||
setMessage('config', {type: 'success', text: 'Configuration sauvegardée'});
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error);
|
||||
setMessage('config', {type: 'error', text: 'Sauvegarde échouée'});
|
||||
} finally {
|
||||
setIsSavingConfig(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const formatDate = (date: string | Date): string => {
|
||||
return new Date(date).toLocaleString();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="p-6">Loading backup settings...</div>;
|
||||
}
|
||||
|
||||
if (!data || !config) {
|
||||
return <div className="p-6">Failed to load backup settings</div>;
|
||||
}
|
||||
|
||||
const getNextBackupTime = (): string => {
|
||||
if (!data?.scheduler.nextBackup) return 'Non planifiée';
|
||||
|
||||
const nextBackup = new Date(data.scheduler.nextBackup);
|
||||
const now = new Date();
|
||||
const diffMs = nextBackup.getTime() - now.getTime();
|
||||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `dans ${diffMins}min`;
|
||||
} else {
|
||||
return `dans ${diffHours}h ${diffMins % 60}min`;
|
||||
}
|
||||
};
|
||||
|
||||
// Composant pour les messages inline
|
||||
const InlineMessage = ({ messageKey }: { messageKey: string }) => {
|
||||
const message = messages[messageKey];
|
||||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<div className={`text-xs mt-2 px-2 py-1 rounded transition-all inline-block ${
|
||||
message.type === 'success'
|
||||
? 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800/20'
|
||||
: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800/20'
|
||||
}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)]">
|
||||
<Header
|
||||
title="TowerControl"
|
||||
subtitle="Gestion des sauvegardes"
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-4 text-sm">
|
||||
<Link href="/settings" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
|
||||
Paramètres
|
||||
</Link>
|
||||
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
|
||||
<Link href="/settings/advanced" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
|
||||
Avancé
|
||||
</Link>
|
||||
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
|
||||
<span className="text-[var(--foreground)]">Sauvegardes</span>
|
||||
</div>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] mb-2">
|
||||
💾 Gestion des sauvegardes
|
||||
</h1>
|
||||
<p className="text-[var(--muted-foreground)]">
|
||||
Configuration et gestion des sauvegardes automatiques de votre base de données
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Layout en 2 colonnes pour optimiser l'espace */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
|
||||
{/* Colonne principale: Configuration */}
|
||||
<div className="xl:col-span-2 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<span className="text-blue-600">⚙️</span>
|
||||
Configuration automatique
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Paramètres des sauvegardes programmées
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--foreground)]">
|
||||
Sauvegardes automatiques
|
||||
</label>
|
||||
<select
|
||||
value={config.enabled ? 'enabled' : 'disabled'}
|
||||
onChange={(e) => setConfig({
|
||||
...config,
|
||||
enabled: e.target.value === 'enabled'
|
||||
})}
|
||||
className="w-full px-3 py-2 bg-[var(--background)] border border-[var(--border)] rounded-md text-[var(--foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
>
|
||||
<option value="enabled">Activées</option>
|
||||
<option value="disabled">Désactivées</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--foreground)]">
|
||||
Fréquence
|
||||
</label>
|
||||
<select
|
||||
value={config.interval}
|
||||
onChange={(e) => setConfig({
|
||||
...config,
|
||||
interval: e.target.value as BackupConfig['interval']
|
||||
})}
|
||||
className="w-full px-3 py-2 bg-[var(--background)] border border-[var(--border)] rounded-md text-[var(--foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
>
|
||||
<option value="hourly">Toutes les heures</option>
|
||||
<option value="daily">Quotidienne</option>
|
||||
<option value="weekly">Hebdomadaire</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--foreground)]">
|
||||
Rétention (nombre max)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={config.maxBackups}
|
||||
onChange={(e) => setConfig({
|
||||
...config,
|
||||
maxBackups: parseInt(e.target.value) || 7
|
||||
})}
|
||||
className="bg-[var(--background)] border-[var(--border)] text-[var(--foreground)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--foreground)]">
|
||||
Compression
|
||||
</label>
|
||||
<select
|
||||
value={config.compression ? 'enabled' : 'disabled'}
|
||||
onChange={(e) => setConfig({
|
||||
...config,
|
||||
compression: e.target.value === 'enabled'
|
||||
})}
|
||||
className="w-full px-3 py-2 bg-[var(--background)] border border-[var(--border)] rounded-md text-[var(--foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
>
|
||||
<option value="enabled">Activée (gzip)</option>
|
||||
<option value="disabled">Désactivée</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-[var(--border)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
onClick={handleSaveConfig}
|
||||
disabled={isSavingConfig}
|
||||
className="bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-[var(--primary-foreground)]"
|
||||
>
|
||||
{isSavingConfig ? 'Sauvegarde...' : 'Sauvegarder'}
|
||||
</Button>
|
||||
<InlineMessage messageKey="config" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions manuelles */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<span className="text-green-600">🚀</span>
|
||||
Actions manuelles
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Créer une sauvegarde ou vérifier l'intégrité de la base
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
onClick={handleCreateBackup}
|
||||
disabled={isCreatingBackup}
|
||||
className="bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-[var(--primary-foreground)]"
|
||||
>
|
||||
{isCreatingBackup ? 'Création...' : 'Créer une sauvegarde'}
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleVerifyDatabase}
|
||||
disabled={isVerifying}
|
||||
className="bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] hover:bg-[var(--muted)]"
|
||||
>
|
||||
{isVerifying ? 'Vérification...' : 'Vérifier l\'intégrité'}
|
||||
</Button>
|
||||
<InlineMessage messageKey="verify" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleToggleScheduler}
|
||||
className={data.scheduler.isRunning
|
||||
? 'bg-[var(--destructive)] hover:bg-[var(--destructive)]/90 text-[var(--destructive-foreground)]'
|
||||
: 'bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-[var(--primary-foreground)]'
|
||||
}
|
||||
>
|
||||
{data.scheduler.isRunning ? 'Arrêter le planificateur' : 'Démarrer le planificateur'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Colonne latérale: Statut et historique */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Statut du système */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">📊 Statut système</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Status planificateur */}
|
||||
<div className="p-3 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`h-2 w-2 rounded-full ${
|
||||
data.scheduler.isRunning ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}></span>
|
||||
<span className="text-sm font-medium">
|
||||
{data.scheduler.isRunning ? 'Actif' : 'Arrêté'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Prochaine: {getNextBackupTime()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Statistiques */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<p className="text-lg font-bold text-[var(--primary)]">{data.backups.length}</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">sauvegardes</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<p className="text-lg font-bold text-[var(--primary)]">{config.maxBackups}</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">max conservées</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chemin */}
|
||||
<div>
|
||||
<p className="text-xs font-medium text-[var(--muted-foreground)] mb-1">Stockage</p>
|
||||
<code className="text-xs bg-[var(--muted)] text-[var(--muted-foreground)] px-2 py-1 rounded border block truncate">
|
||||
{data.config.backupPath}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Historique des sauvegardes */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">📁 Historique</h3>
|
||||
<InlineMessage messageKey="restore" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.backups.length === 0 ? (
|
||||
<p className="text-sm text-[var(--muted-foreground)] text-center py-4">
|
||||
Aucune sauvegarde disponible
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{data.backups.slice(0, 10).map((backup) => (
|
||||
<div
|
||||
key={backup.id}
|
||||
className="p-3 bg-[var(--card)] rounded border border-[var(--border)]"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-[var(--foreground)] truncate">
|
||||
{backup.filename.replace('towercontrol_', '').replace('.db.gz', '').replace('.db', '')}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
{formatFileSize(backup.size)}
|
||||
</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||
backup.type === 'manual'
|
||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||
}`}>
|
||||
{backup.type === 'manual' ? 'Manuel' : 'Auto'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
{formatDate(backup.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
{process.env.NODE_ENV !== 'production' && (
|
||||
<Button
|
||||
onClick={() => setShowRestoreConfirm(backup.filename)}
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white text-xs px-2 py-1 h-auto"
|
||||
>
|
||||
↻
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirm(backup.filename)}
|
||||
className="bg-red-500 hover:bg-red-600 text-white text-xs px-2 py-1 h-auto"
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{data.backups.length > 10 && (
|
||||
<p className="text-xs text-[var(--muted-foreground)] text-center pt-2">
|
||||
... et {data.backups.length - 10} autres
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal de confirmation de restauration */}
|
||||
{showRestoreConfirm && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => setShowRestoreConfirm(null)}
|
||||
title="Confirmer la restauration"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
⚠️ <strong>Attention :</strong> Cette action va remplacer complètement
|
||||
la base de données actuelle par la sauvegarde sélectionnée.
|
||||
</p>
|
||||
<p>
|
||||
Une sauvegarde de la base actuelle sera créée automatiquement
|
||||
avant la restauration.
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
Fichier à restaurer: <code>{showRestoreConfirm}</code>
|
||||
</p>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button
|
||||
onClick={() => setShowRestoreConfirm(null)}
|
||||
className="bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]"
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleRestoreBackup(showRestoreConfirm)}
|
||||
className="bg-[var(--warning)] hover:bg-[var(--warning)]/90 text-[var(--warning-foreground)]"
|
||||
>
|
||||
Confirmer la restauration
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Modal de confirmation de suppression */}
|
||||
{showDeleteConfirm && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => setShowDeleteConfirm(null)}
|
||||
title="Confirmer la suppression"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
Êtes-vous sûr de vouloir supprimer cette sauvegarde ?
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
Fichier: <code>{showDeleteConfirm}</code>
|
||||
</p>
|
||||
<p className="text-red-600 text-sm">
|
||||
Cette action est irréversible.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirm(null)}
|
||||
className="bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]"
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDeleteBackup(showDeleteConfirm)}
|
||||
className="bg-[var(--destructive)] hover:bg-[var(--destructive)]/90 text-[var(--destructive-foreground)]"
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user