Files
picpeak/frontend/src/components/admin/BackupDashboard.jsx
T
paul 20dd43c093
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Failing after 2m18s
Version and Release / version-bump (push) Successful in 42s
Version and Release / trigger-drone (push) Successful in 3s
feat: implement comprehensive backup and restore system with S3 support
- Add S3/MinIO storage adapter with multipart upload support
- Implement database backup service for SQLite and PostgreSQL
- Create backup manifest generator for tracking backup contents
- Enhance backup service with S3 integration and incremental backups
- Add restore service with safety measures and rollback capability
- Create comprehensive test suite for all backup functionality
- Add admin API endpoints for backup/restore management
- Implement frontend UI with dashboard, configuration, and restore wizard
- Add roadmap section to README with implemented backup feature

This implementation provides:
- Multiple backup destinations (local, rsync, S3/MinIO)
- Intelligent change detection to minimize backup frequency
- Full database backups with compression
- Manifest-based restore with integrity validation
- Pre-restore safety backups with rollback
- Comprehensive error handling and monitoring
- User-friendly admin interface

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 09:05:52 +02:00

325 lines
12 KiB
React

import React from 'react';
import {
HardDrive,
Database,
FileArchive,
Image,
Clock,
CheckCircle,
AlertCircle,
TrendingUp,
Shield,
Server,
Cloud,
Calendar,
Play,
Loader2,
AlertTriangle,
Info
} from 'lucide-react';
import { format, formatDistanceToNow } from 'date-fns';
import { Card, Button } from '../common';
const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
<Card className="p-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-gray-600">{label}</p>
<p className="mt-2 text-3xl font-semibold text-gray-900">{value}</p>
{subtext && (
<p className="mt-1 text-sm text-gray-500">{subtext}</p>
)}
</div>
<div className={`p-3 bg-${color}-100 rounded-lg`}>
<Icon className={`h-6 w-6 text-${color}-600`} />
</div>
</div>
</Card>
);
const formatBytes = (bytes) => {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }) => {
const lastBackup = status?.lastBackup;
const statistics = lastBackup?.statistics || {};
const isConfigured = config && config.backup_destination_type;
const isEnabled = config?.backup_enabled;
// Calculate backup health score
const getHealthScore = () => {
if (!lastBackup) return { score: 0, status: 'critical', message: 'No backups found' };
const hoursSinceBackup = (Date.now() - new Date(lastBackup.created_at)) / (1000 * 60 * 60);
if (lastBackup.status === 'failed') {
return { score: 0, status: 'critical', message: 'Last backup failed' };
}
if (hoursSinceBackup < 24) {
return { score: 100, status: 'excellent', message: 'Backup is up to date' };
} else if (hoursSinceBackup < 48) {
return { score: 75, status: 'good', message: 'Backup is recent' };
} else if (hoursSinceBackup < 168) { // 1 week
return { score: 50, status: 'warning', message: 'Backup is getting old' };
} else {
return { score: 25, status: 'critical', message: 'Backup is outdated' };
}
};
const health = getHealthScore();
const healthColors = {
excellent: 'green',
good: 'blue',
warning: 'amber',
critical: 'red'
};
return (
<div className="space-y-6">
{/* Configuration Alert */}
{!isConfigured && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex">
<AlertTriangle className="h-5 w-5 text-amber-400 mt-0.5" />
<div className="ml-3">
<h3 className="text-sm font-medium text-amber-800">
Backup Not Configured
</h3>
<p className="mt-1 text-sm text-amber-700">
Please configure backup settings in the Configuration tab before running backups.
</p>
</div>
</div>
</div>
)}
{/* Health Score Card */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Backup Health</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium bg-${healthColors[health.status]}-100 text-${healthColors[health.status]}-700`}>
{health.status.charAt(0).toUpperCase() + health.status.slice(1)}
</span>
</div>
<div className="flex items-center space-x-4">
<div className="relative w-24 h-24">
<svg className="w-24 h-24 transform -rotate-90">
<circle
cx="48"
cy="48"
r="36"
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-gray-200"
/>
<circle
cx="48"
cy="48"
r="36"
stroke="currentColor"
strokeWidth="8"
fill="none"
strokeDasharray={`${(health.score / 100) * 226} 226`}
className={`text-${healthColors[health.status]}-500`}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-gray-900">{health.score}%</span>
</div>
</div>
<div className="flex-1">
<p className="text-gray-700 font-medium">{health.message}</p>
{lastBackup && (
<p className="text-sm text-gray-500 mt-1">
Last successful backup: {formatDistanceToNow(new Date(lastBackup.created_at), { addSuffix: true })}
</p>
)}
<Button
onClick={onRunBackup}
disabled={!isConfigured || !isEnabled || isBackupRunning}
className="mt-3"
size="sm"
>
{isBackupRunning ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Running...
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Run Backup Now
</>
)}
</Button>
</div>
</div>
</Card>
{/* Statistics Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
icon={FileArchive}
label="Total Backups"
value={status?.totalBackups || 0}
color="blue"
subtext={lastBackup ? `Last: ${format(new Date(lastBackup.created_at), 'PP')}` : 'No backups yet'}
/>
<StatCard
icon={HardDrive}
label="Backup Size"
value={formatBytes(statistics.total_size || 0)}
color="green"
subtext={`${statistics.files_processed || 0} files`}
/>
<StatCard
icon={Clock}
label="Last Duration"
value={lastBackup ? `${Math.round(lastBackup.duration_seconds / 60)}m` : 'N/A'}
color="purple"
subtext={lastBackup ? format(new Date(lastBackup.created_at), 'p') : ''}
/>
<StatCard
icon={Shield}
label="Backup Status"
value={isEnabled ? 'Active' : 'Inactive'}
color={isEnabled ? 'green' : 'gray'}
subtext={config?.backup_destination_type || 'Not configured'}
/>
</div>
{/* Recent Activity */}
{status?.recentBackups && status.recentBackups.length > 0 && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Backup Activity</h3>
<div className="space-y-3">
{status.recentBackups.slice(0, 5).map((backup) => (
<div key={backup.id} className="flex items-center justify-between py-3 border-b border-gray-100 last:border-0">
<div className="flex items-center space-x-3">
{backup.status === 'completed' ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : backup.status === 'failed' ? (
<AlertCircle className="h-5 w-5 text-red-500" />
) : (
<Loader2 className="h-5 w-5 text-blue-500 animate-spin" />
)}
<div>
<p className="font-medium text-gray-900">
{backup.backup_type} backup
</p>
<p className="text-sm text-gray-500">
{format(new Date(backup.created_at), 'PPp')}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">
{formatBytes(backup.statistics?.total_size || 0)}
</p>
<p className="text-sm text-gray-500">
{backup.statistics?.files_processed || 0} files
</p>
</div>
</div>
))}
</div>
</Card>
)}
{/* Storage Status */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Backup Coverage</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Database className="h-5 w-5 text-gray-400" />
<span className="text-gray-700">Database</span>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${
statistics.database_backed_up ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
}`}>
{statistics.database_backed_up ? 'Backed up' : 'Not backed up'}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Image className="h-5 w-5 text-gray-400" />
<span className="text-gray-700">Photos</span>
</div>
<span className="text-sm text-gray-500">
{statistics.photos_backed_up || 0} of {statistics.total_photos || 0}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<FileArchive className="h-5 w-5 text-gray-400" />
<span className="text-gray-700">Archives</span>
</div>
<span className="text-sm text-gray-500">
{statistics.archives_backed_up || 0} files
</span>
</div>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Destination Info</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3">
{config?.backup_destination_type === 's3' ? (
<Cloud className="h-5 w-5 text-blue-500" />
) : config?.backup_destination_type === 'rsync' ? (
<Server className="h-5 w-5 text-purple-500" />
) : (
<HardDrive className="h-5 w-5 text-gray-500" />
)}
<div>
<p className="font-medium text-gray-900">
{config?.backup_destination_type
? config.backup_destination_type.toUpperCase()
: 'Not Configured'}
</p>
<p className="text-sm text-gray-500">
{config?.backup_destination_type === 's3' && config?.backup_s3_bucket
? `Bucket: ${config.backup_s3_bucket}`
: config?.backup_destination_type === 'local' && config?.backup_destination_path
? `Path: ${config.backup_destination_path}`
: config?.backup_destination_type === 'rsync' && config?.backup_rsync_host
? `Host: ${config.backup_rsync_host}`
: 'No destination set'}
</p>
</div>
</div>
{config?.backup_retention_days && (
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-2">
<Info className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600">
Backups retained for {config.backup_retention_days} days
</span>
</div>
</div>
)}
</div>
</Card>
</div>
</div>
);
};