chore(release): snapshot 1.4.2

This commit is contained in:
Erik Silva
2025-12-17 13:36:23 -03:00
parent 2a112f169d
commit 99d828869a
95 changed files with 9933 additions and 1601 deletions

View File

@@ -0,0 +1,385 @@
"use client";
import { useState, useEffect } from "react";
import {
ArrowDownTrayIcon,
ArrowUpTrayIcon,
ClockIcon,
ServerIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
BoltIcon,
} from '@heroicons/react/24/outline';
interface Backup {
filename: string;
size: string;
date: string;
timestamp: string;
}
export default function BackupPage() {
const [loading, setLoading] = useState(false);
const [backups, setBackups] = useState<Backup[]>([]);
const [selectedBackup, setSelectedBackup] = useState<string>("");
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [autoBackupEnabled, setAutoBackupEnabled] = useState(false);
const [autoBackupInterval, setAutoBackupInterval] = useState<number>(6);
useEffect(() => {
loadBackups();
loadAutoBackupSettings();
}, []);
const loadAutoBackupSettings = () => {
const enabled = localStorage.getItem('autoBackupEnabled') === 'true';
const interval = parseInt(localStorage.getItem('autoBackupInterval') || '6');
setAutoBackupEnabled(enabled);
setAutoBackupInterval(interval);
};
const toggleAutoBackup = () => {
const newValue = !autoBackupEnabled;
setAutoBackupEnabled(newValue);
localStorage.setItem('autoBackupEnabled', newValue.toString());
if (newValue) {
startAutoBackup();
setMessage({ type: 'success', text: `Backup automático ativado (a cada ${autoBackupInterval}h)` });
} else {
stopAutoBackup();
setMessage({ type: 'success', text: 'Backup automático desativado' });
}
};
const startAutoBackup = () => {
const intervalMs = autoBackupInterval * 60 * 60 * 1000; // horas para ms
const intervalId = setInterval(() => {
createBackup();
}, intervalMs);
localStorage.setItem('autoBackupIntervalId', intervalId.toString());
};
const stopAutoBackup = () => {
const intervalId = localStorage.getItem('autoBackupIntervalId');
if (intervalId) {
clearInterval(parseInt(intervalId));
localStorage.removeItem('autoBackupIntervalId');
}
};
const changeInterval = (hours: number) => {
setAutoBackupInterval(hours);
localStorage.setItem('autoBackupInterval', hours.toString());
if (autoBackupEnabled) {
stopAutoBackup();
startAutoBackup();
setMessage({ type: 'success', text: `Intervalo alterado para ${hours}h` });
}
};
const loadBackups = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:8085/api/superadmin/backups', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
setBackups(data.backups || []);
}
} catch (error) {
console.error('Erro ao carregar backups:', error);
}
};
const createBackup = async () => {
setLoading(true);
setMessage(null);
try {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:8085/api/superadmin/backup/create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (response.ok) {
setMessage({ type: 'success', text: `Backup criado: ${data.filename} (${data.size})` });
await loadBackups();
} else {
setMessage({ type: 'error', text: data.error || 'Erro ao criar backup' });
}
} catch (error) {
setMessage({ type: 'error', text: 'Erro ao criar backup' });
} finally {
setLoading(false);
}
};
const restoreBackup = async () => {
if (!selectedBackup) {
setMessage({ type: 'error', text: 'Selecione um backup para restaurar' });
return;
}
if (!confirm(`⚠️ ATENÇÃO: Isso irá SOBRESCREVER todos os dados atuais!\n\nDeseja restaurar o backup:\n${selectedBackup}?`)) {
return;
}
setLoading(true);
setMessage(null);
try {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:8085/api/superadmin/backup/restore', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename: selectedBackup })
});
const data = await response.json();
if (response.ok) {
setMessage({ type: 'success', text: 'Backup restaurado com sucesso! Recarregando...' });
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
setMessage({ type: 'error', text: data.error || 'Erro ao restaurar backup' });
}
} catch (error) {
setMessage({ type: 'error', text: 'Erro ao restaurar backup' });
} finally {
setLoading(false);
}
};
const downloadBackup = async (filename: string) => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://localhost:8085/api/superadmin/backup/download/${filename}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} else {
setMessage({ type: 'error', text: 'Erro ao baixar backup' });
}
} catch (error) {
setMessage({ type: 'error', text: 'Erro ao baixar backup' });
}
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Backup & Restore
</h1>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Gerencie backups do banco de dados PostgreSQL
</p>
</div>
{message && (
<div className={`mb-6 p-4 rounded-lg flex items-center gap-3 ${message.type === 'success'
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{message.type === 'success' ? (
<CheckCircleIcon className="h-5 w-5" />
) : (
<ExclamationTriangleIcon className="h-5 w-5" />
)}
<span>{message.text}</span>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<ArrowDownTrayIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Criar Backup
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Exportar dados atuais
</p>
</div>
</div>
<button
onClick={createBackup}
disabled={loading}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{loading ? 'Criando...' : 'Criar Novo Backup'}
</button>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-green-100 dark:bg-green-900/30 rounded-lg">
<BoltIcon className="h-6 w-6 text-green-600 dark:text-green-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Backup Automático
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
{autoBackupEnabled ? `Ativo (${autoBackupInterval}h)` : 'Desativado'}
</p>
</div>
</div>
<div className="space-y-3">
<select
value={autoBackupInterval}
onChange={(e) => changeInterval(parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value={1}>A cada 1 hora</option>
<option value={3}>A cada 3 horas</option>
<option value={6}>A cada 6 horas</option>
<option value={12}>A cada 12 horas</option>
<option value={24}>A cada 24 horas</option>
</select>
<button
onClick={toggleAutoBackup}
className={`w-full px-4 py-2 rounded-lg transition-colors ${
autoBackupEnabled
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
>
{autoBackupEnabled ? 'Desativar' : 'Ativar'}
</button>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
<ArrowUpTrayIcon className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Restaurar Backup
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Sobrescreve dados atuais
</p>
</div>
</div>
<select
value={selectedBackup}
onChange={(e) => setSelectedBackup(e.target.value)}
className="w-full mb-3 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="">Selecione um backup...</option>
{backups.map((backup) => (
<option key={backup.filename} value={backup.filename}>
{backup.filename} - {backup.size}
</option>
))}
</select>
<button
onClick={restoreBackup}
disabled={loading || !selectedBackup}
className="w-full px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{loading ? 'Restaurando...' : 'Restaurar Backup'}
</button>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<ServerIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Backups Disponíveis
</h2>
<span className="ml-auto text-sm text-gray-500 dark:text-gray-400">
{backups.length} arquivo(s)
</span>
</div>
</div>
<div className="p-6">
{backups.length === 0 ? (
<div className="text-center py-12">
<ServerIcon className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Nenhum backup encontrado
</p>
</div>
) : (
<div className="space-y-3">
{backups.map((backup) => (
<div
key={backup.filename}
className={`p-4 rounded-lg border transition-colors cursor-pointer ${selectedBackup === backup.filename
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
onClick={() => setSelectedBackup(backup.filename)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<ClockIcon className="h-5 w-5 text-gray-400" />
<div>
<p className="font-medium text-gray-900 dark:text-white">
{backup.filename}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{backup.date} {backup.size}
</p>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
downloadBackup(backup.filename);
}}
className="px-3 py-1 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded"
>
Download
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}