Checkpoint: Ajout onglet Sauvegarde DB dans ImportSettings : endpoint POST /api/db-backup (mysqldump), GET /api/db-backup/:filename, procédures tRPC backup.list et backup.delete, composant BackupSection avec liste des sauvegardes et téléchargement.

This commit is contained in:
Manus
2026-08-08 10:04:57 +00:00
parent 711ce6b83a
commit 4ce2d2877a
3 changed files with 246 additions and 2 deletions

View File

@@ -11,10 +11,146 @@ import { toast } from "sonner";
import {
Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download,
Inbox, CheckCircle2, Monitor, FolderOutput, Wifi, AlertTriangle, Calendar,
ArrowDownToLine, Share2
ArrowDownToLine, Share2, DatabaseBackup, HardDrive, CheckCircle, Clock
} from "lucide-react";
import DashboardLayout from "@/components/DashboardLayout";
// ============= COMPOSANT SAUVEGARDE DB =============
function BackupSection() {
const [isGenerating, setIsGenerating] = useState(false);
const { data: backupList, refetch: refetchList } = trpc.backup.list.useQuery();
const deleteBackupMutation = trpc.backup.delete.useMutation({
onSuccess: () => { refetchList(); toast.success("Sauvegarde supprimée"); },
onError: (e) => toast.error("Erreur : " + e.message),
});
const handleGenerateBackup = async () => {
setIsGenerating(true);
try {
const response = await fetch("/api/db-backup", { method: "POST", credentials: "include" });
if (!response.ok) {
const err = await response.json().catch(() => ({ error: "Erreur inconnue" }));
toast.error("Erreur : " + (err.error || response.statusText));
return;
}
// Déclencher le téléchargement
const blob = await response.blob();
const contentDisposition = response.headers.get("Content-Disposition") || "";
const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?["']?([^"';\n]+)/i);
const fileName = match ? decodeURIComponent(match[1]) : `backup-${new Date().toISOString().slice(0,10)}.sql`;
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
toast.success("Sauvegarde générée et téléchargée");
refetchList();
} catch (e: any) {
toast.error("Erreur : " + e.message);
} finally {
setIsGenerating(false);
}
};
const formatSize = (bytes: number) => {
if (bytes < 1024) return bytes + " o";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " Ko";
return (bytes / (1024 * 1024)).toFixed(1) + " Mo";
};
return (
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-slate-50 to-gray-50 dark:from-slate-950/20 dark:to-gray-950/20 border-b">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-600 rounded-lg">
<DatabaseBackup className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Sauvegarde de la base de données</CardTitle>
<CardDescription className="mt-1">
Générer un dump SQL de la base de données et l'enregistrer localement dans le dossier <code className="bg-muted px-1 rounded text-xs">backups/</code>
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="pt-6 space-y-6">
{/* Bouton générer */}
<div className="flex items-center gap-4">
<Button
onClick={handleGenerateBackup}
disabled={isGenerating}
className="gap-2 bg-slate-700 hover:bg-slate-800 text-white"
size="lg"
>
{isGenerating ? (
<><Loader2 className="h-5 w-5 animate-spin" />Génération en cours...</>
) : (
<><HardDrive className="h-5 w-5" />Générer une sauvegarde</>
)}
</Button>
<p className="text-sm text-muted-foreground">
Le dump SQL sera généré, enregistré dans <code className="bg-muted px-1 rounded text-xs">backups/</code> et téléchargé automatiquement.
</p>
</div>
{/* Liste des sauvegardes existantes */}
{backupList && backupList.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Clock className="h-4 w-4" />
Sauvegardes enregistrées ({backupList.length})
</div>
<div className="border rounded-lg divide-y">
{backupList.map((backup) => (
<div key={backup.name} className="flex items-center justify-between px-4 py-3 hover:bg-muted/30">
<div className="flex items-center gap-3">
<CheckCircle className="h-4 w-4 text-green-500 shrink-0" />
<div>
<p className="text-sm font-medium font-mono">{backup.name}</p>
<p className="text-xs text-muted-foreground">
{new Date(backup.createdAt).toLocaleString("fr-FR")} — {formatSize(backup.size)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="gap-1 text-xs"
onClick={() => { window.open(`/api/db-backup/${encodeURIComponent(backup.name)}`, "_blank"); }}
>
<Download className="h-3 w-3" />
Télécharger
</Button>
<Button
variant="outline"
size="sm"
className="gap-1 text-xs text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => deleteBackupMutation.mutate({ name: backup.name })}
disabled={deleteBackupMutation.isPending}
>
Supprimer
</Button>
</div>
</div>
))}
</div>
</div>
)}
{backupList && backupList.length === 0 && (
<div className="text-center py-8 text-muted-foreground text-sm">
<HardDrive className="h-8 w-8 mx-auto mb-2 opacity-30" />
Aucune sauvegarde enregistrée
</div>
)}
</CardContent>
</Card>
);
}
export default function ImportSettings() {
// Chargement différé : on ne charge les statuts de services qu'après le montage
const { data: settings, isLoading } = trpc.importSettings.get.useQuery(undefined, {
@@ -207,7 +343,7 @@ export default function ImportSettings() {
{/* Onglets Import / Export */}
<Tabs defaultValue="import" className="w-full">
<TabsList className="grid w-full grid-cols-2 h-12 mb-6">
<TabsList className="grid w-full grid-cols-3 h-12 mb-6">
<TabsTrigger value="import" className="flex items-center gap-2 text-base">
<ArrowDownToLine className="w-4 h-4" />
Paramètres d'import
@@ -216,6 +352,10 @@ export default function ImportSettings() {
<Share2 className="w-4 h-4" />
Paramètres d'export
</TabsTrigger>
<TabsTrigger value="backup" className="flex items-center gap-2 text-base">
<DatabaseBackup className="w-4 h-4" />
Sauvegarde DB
</TabsTrigger>
</TabsList>
{/* ===== ONGLET IMPORT ===== */}
@@ -955,6 +1095,10 @@ export default function ImportSettings() {
</Button>
</div>
</TabsContent>
{/* ===== ONGLET SAUVEGARDE ===== */}
<TabsContent value="backup" className="space-y-6">
<BackupSection />
</TabsContent>
</Tabs>
</div>
</DashboardLayout>