Files
nixpanel-server/nixserver/src/pages/Backup.tsx
T

101 lines
5.1 KiB
TypeScript

import { useEffect, useState } from 'react'
import { HardDrive, Trash2, RefreshCw, Play } from 'lucide-react'
import { api } from '../api/client'
interface Backup { path: string; filename: string; size: number; createdAt: string }
const fmtSize = (b: number) => b > 1024 * 1024 * 1024 ? `${(b / 1024 / 1024 / 1024).toFixed(1)} GB` : `${(b / 1024 / 1024).toFixed(0)} MB`
export default function Backup() {
const [backups, setBackups] = useState<Backup[]>([])
const [loading, setLoading] = useState(false)
const [backing, setBacking] = useState(false)
const [accounts, setAccounts] = useState<{ id: number; username: string }[]>([])
const [selectedAccount, setSelectedAccount] = useState('')
const fetch = async () => {
setLoading(true)
try {
const [b, a] = await Promise.all([api.get('/nixserver/backup/list'), api.get('/nixserver/accounts?limit=100')])
setBackups(b.data.data)
setAccounts(a.data.data.items)
} finally { setLoading(false) }
}
useEffect(() => { fetch() }, [])
const backupAccount = async () => {
if (!selectedAccount) return alert('Select an account')
setBacking(true)
try { await api.post(`/nixserver/backup/account/${selectedAccount}`); fetch() }
catch (err: unknown) { alert((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Backup failed') }
finally { setBacking(false) }
}
const backupServer = async () => {
if (!confirm('Start a full server backup? This may take a while.')) return
setBacking(true)
try { await api.post('/nixserver/backup/server'); fetch() }
catch (err: unknown) { alert((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Backup failed') }
finally { setBacking(false) }
}
const del = async (path: string) => {
if (!confirm('Delete this backup?')) return
try { await api.delete('/nixserver/backup', { data: { path } }); fetch() }
catch {}
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div><h1 className="text-white text-xl font-semibold">Backup</h1><p className="text-[#64748b] text-sm">{backups.length} backup files</p></div>
<button onClick={fetch} className="p-2 text-[#64748b] hover:text-white border border-[#2a2d3e] rounded-lg"><RefreshCw size={16} className={loading ? 'animate-spin' : ''} /></button>
</div>
{/* Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
<h2 className="text-white text-sm font-medium mb-3">Account Backup</h2>
<div className="flex gap-2">
<select value={selectedAccount} onChange={e => setSelectedAccount(e.target.value)} className="flex-1 bg-[#0f1117] border border-[#2a2d3e] text-[#94a3b8] text-sm rounded px-3 py-2 focus:outline-none focus:border-indigo-500">
<option value="">Select account...</option>
{accounts.map(a => <option key={a.id} value={a.id}>{a.username}</option>)}
</select>
<button onClick={backupAccount} disabled={backing} className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white text-sm px-3 py-2 rounded-lg">
<Play size={14} /> {backing ? 'Backing up...' : 'Backup'}
</button>
</div>
</div>
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
<h2 className="text-white text-sm font-medium mb-3">Full Server Backup</h2>
<p className="text-[#64748b] text-xs mb-3">Backs up /etc, /home, and /var/www</p>
<button onClick={backupServer} disabled={backing} className="flex items-center gap-2 bg-amber-600/20 hover:bg-amber-600/30 text-amber-400 border border-amber-500/30 text-sm px-3 py-2 rounded-lg disabled:opacity-50">
<HardDrive size={14} /> Full Backup
</button>
</div>
</div>
{/* Backup list */}
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead><tr className="border-b border-[#2a2d3e]">
{['Filename', 'Size', 'Created', ''].map(h => <th key={h} className="text-left text-[#64748b] font-medium px-4 py-3 text-xs">{h}</th>)}
</tr></thead>
<tbody>
{backups.map(b => (
<tr key={b.path} className="border-b border-[#2a2d3e] last:border-0 hover:bg-[#1e2130]">
<td className="px-4 py-3 font-mono text-white text-xs">{b.filename}</td>
<td className="px-4 py-3 text-[#64748b]">{fmtSize(b.size)}</td>
<td className="px-4 py-3 text-[#64748b] text-xs">{new Date(b.createdAt).toLocaleString()}</td>
<td className="px-4 py-3"><button onClick={() => del(b.path)} className="p-1 text-[#64748b] hover:text-red-400 rounded"><Trash2 size={14} /></button></td>
</tr>
))}
{!loading && backups.length === 0 && <tr><td colSpan={4} className="text-center text-[#64748b] py-8">No backups</td></tr>}
</tbody>
</table>
</div>
</div>
)
}