Compare commits

...

11 Commits

Author SHA1 Message Date
Gitea Actions Bot 8b0c1655a1 chore: bump frontend version to 1.0.88
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2025-07-23 10:10:42 +00:00
paul ff950244d5 fix: complete restore page translations and fix structure
Mirror to GitHub / mirror (push) Successful in 39s
Test and Lint / backend-test (push) Successful in 1m23s
Test and Lint / frontend-test (push) Successful in 2m0s
Version and Release / version-bump (push) Successful in 45s
Version and Release / trigger-drone (push) Successful in 3s
continuous-integration/drone/push Build is passing
- Fix restoreTypes translation structure (was under options.types)
- Add missing restore.messages.restoreStarted translation
- Ensure all restore wizard strings use translations
- Add corresponding German translations for restore section
- Fix translation key structure to match component expectations

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 12:05:48 +02:00
Gitea Actions Bot cd3d6fc704 chore: bump frontend version to 1.0.87
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-23 10:03:33 +00:00
paul c536fe45ff fix: complete backup page translations and improve UI
Mirror to GitHub / mirror (push) Successful in 36s
Test and Lint / backend-test (push) Successful in 1m32s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 1m56s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Fix '0 files' hardcoded string to use translation
- Fix 'local' destination type to show translated name
- Add missing field placeholders for rsync and S3 configurations
- Add missing backup.history.columns.* translations
- Add missing backup.history.filter.* translations
- Add missing backup.history.details.* translations
- Fix backup destination display to use proper translation key
- Replace TestTube icon with Wifi icon for connection testing
- Add all corresponding German translations
- Ensure Backup Health and Coverage titles use translations

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 11:58:31 +02:00
paul 42c8ef1c0e docs: add minimum system requirements section to README
Mirror to GitHub / mirror (push) Successful in 47s
Test and Lint / backend-test (push) Successful in 1m35s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 1m58s
- Add CPU, RAM, and storage requirements
- Include OS and software dependencies
- Add Docker requirements for containerized deployment
- Keep it concise and focused on minimum requirements only

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 11:18:27 +02:00
paul d07d742528 fix: use plugins/github-release for Drone CI/CD
Mirror to GitHub / mirror (push) Successful in 50s
Test and Lint / backend-test (push) Successful in 1m46s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
- Replace manual curl approach with plugins/github-release
- Fixes shell parsing issues with multiline strings
- Properly passes GITHUB_TOKEN via api_key setting
- Uses YAML multiline string (|) for release notes
- Cleaner and more reliable approach

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 10:56:46 +02:00
Gitea Actions Bot dbc9f0a605 chore: bump frontend version to 1.0.86
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-23 08:44:17 +00:00
paul 3eb8550cbf fix: add missing translations and fix BackupHistory useTranslation error
Mirror to GitHub / mirror (push) Successful in 43s
Test and Lint / backend-test (push) Successful in 1m40s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 1m59s
Version and Release / version-bump (push) Successful in 1m31s
Version and Release / trigger-drone (push) Successful in 3s
- Add missing useTranslation hook in BackupHistory.jsx
- Add missing translation keys:
  - backup.dashboard.health.title
  - backup.dashboard.coverage.title
  - backup.dashboard.stats.noBackupsYet
  - backup.configuration.enableBackupHelp
  - backup.configuration.schedule.options.*
  - backup.configuration.messages.*
  - backup.dashboard.noDestinationSet
  - Fix health message keys to match component usage
- Update German translations with same missing keys
- Fix runtime error preventing access to backup history page

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 10:37:39 +02:00
paul bebc045029 fix: simplify Drone github-release step to avoid shell parsing issues
Mirror to GitHub / mirror (push) Successful in 47s
Test and Lint / backend-test (push) Successful in 2m27s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m20s
- Use echo with single JSON string instead of heredoc
- Use > for folded scalar to avoid newline issues
- Properly escape quotes in JSON body
- Ensure GITHUB_TOKEN is properly passed

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 10:31:45 +02:00
Gitea Actions Bot 7325a9e5a2 chore: bump frontend version to 1.0.85
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-23 08:19:38 +00:00
paul d8c229203e feat: add complete translation support for backup admin page
Mirror to GitHub / mirror (push) Successful in 38s
Test and Lint / backend-test (push) Successful in 1m33s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 1m0s
Version and Release / trigger-drone (push) Successful in 3s
- Add comprehensive backup translation keys to en.json and de.json
- Update all backup components to use i18next translations:
  - BackupManagement.jsx: main page with tab navigation
  - BackupDashboard.jsx: health status and statistics
  - BackupConfiguration.jsx: settings and destination configuration
  - BackupHistory.jsx: backup history table and details
  - RestoreWizard.jsx: multi-step restore process
- Replace all hardcoded strings with translation keys
- Support dynamic values with interpolation
- Fix Drone CI/CD github-release step:
  - Write release.json to /tmp to avoid permission issues
  - Use quoted heredoc to prevent shell interpretation errors
  - Replace placeholders with actual tag values using sed

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 10:14:04 +02:00
11 changed files with 1137 additions and 292 deletions
+40 -24
View File
@@ -110,33 +110,49 @@ steps:
# -------- NEW: Create GitHub Release --------
- name: github-release
image: curlimages/curl:latest
image: plugins/github-release
when:
event: tag
environment:
GITHUB_TOKEN:
settings:
api_key:
from_secret: GITHUB_TOKEN
commands:
- |
# Create release payload
cat > release.json <<EOF
{
"tag_name": "${DRONE_TAG}",
"target_commitish": "main",
"name": "PicPeak ${DRONE_TAG}",
"body": "# PicPeak ${DRONE_TAG}\n\n## 🐳 Docker Images\n\nThis release includes Docker images published to GitHub Container Registry:\n\n\`\`\`bash\n# Backend\ndocker pull ghcr.io/the-luap/picpeak-backend:${DRONE_TAG}\ndocker pull ghcr.io/the-luap/picpeak-backend:latest\n\n# Frontend\ndocker pull ghcr.io/the-luap/picpeak-frontend:${DRONE_TAG}\ndocker pull ghcr.io/the-luap/picpeak-frontend:latest\n\`\`\`\n\n## 📦 What's New\n\nSee the [README](https://github.com/the-luap/picpeak#readme) for features and documentation.\n\n## 🚀 Quick Start\n\n\`\`\`bash\n# Clone and deploy\ngit clone https://github.com/the-luap/picpeak.git\ncd picpeak\n\n# Use the tagged version\ndocker-compose -f docker-compose.prod.yml up -d\n\`\`\`\n\n---\n\nFor detailed deployment instructions, see the [Deployment Guide](https://github.com/the-luap/picpeak/blob/main/DEPLOYMENT.md).",
"draft": false,
"prerelease": false
}
EOF
- |
# Create the release on GitHub
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/the-luap/picpeak/releases \
-d @release.json
files: []
title: PicPeak ${DRONE_TAG}
note: |
# PicPeak ${DRONE_TAG}
## 🐳 Docker Images
This release includes Docker images published to GitHub Container Registry:
```bash
# Backend
docker pull ghcr.io/the-luap/picpeak-backend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-backend:latest
# Frontend
docker pull ghcr.io/the-luap/picpeak-frontend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-frontend:latest
```
## 📦 What's New
See the [README](https://github.com/the-luap/picpeak#readme) for features and documentation.
## 🚀 Quick Start
```bash
# Clone and deploy
git clone https://github.com/the-luap/picpeak.git
cd picpeak
# Use the tagged version
docker-compose -f docker-compose.prod.yml up -d
```
---
For detailed deployment instructions, see the [Deployment Guide](https://github.com/the-luap/picpeak/blob/main/DEPLOYMENT.md).
trigger:
event:
+14
View File
@@ -94,6 +94,20 @@ Perfect for:
- **Email**: SMTP with customizable templates
- **Analytics**: Privacy-focused with Umami integration
## 💻 System Requirements
### Minimum Requirements
- **CPU**: 2 CPU cores
- **RAM**: 2GB minimum
- **Storage**: 20GB minimum (plus photo storage needs)
- **OS**: Linux (Ubuntu 20.04+), macOS, or Windows with WSL2
- **Node.js**: v18.0.0 or higher
- **Database**: SQLite (included) or PostgreSQL 12+
### Docker Requirements (Recommended)
- **Docker**: v20.10.0+
- **Docker Compose**: v2.0.0+
## 🤝 Contributing
We love contributions! PicPeak is built by photographers, for photographers. Whether you're fixing bugs, adding features, or improving documentation, your help is welcome.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-frontend",
"version": "1.0.84",
"version": "1.0.88",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "1.0.84",
"version": "1.0.88",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-character-count": "^2.26.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.84",
"version": "1.0.88",
"type": "module",
"scripts": {
"dev": "vite",
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
Save,
Server,
@@ -11,7 +12,7 @@ import {
Info,
Eye,
EyeOff,
TestTube,
Wifi,
CheckCircle,
XCircle,
Loader2,
@@ -23,38 +24,40 @@ import {
import { toast } from 'react-toastify';
import { Button, Card, Input } from '../common';
const destinationTypes = [
{
id: 'local',
name: 'Local Storage',
icon: HardDrive,
description: 'Store backups on the local server filesystem',
fields: ['backup_destination_path']
},
{
id: 'rsync',
name: 'Remote Server (Rsync)',
icon: Server,
description: 'Sync backups to a remote server via SSH/Rsync',
fields: ['backup_rsync_host', 'backup_rsync_user', 'backup_rsync_path', 'backup_rsync_ssh_key']
},
{
id: 's3',
name: 'S3 Compatible Storage',
icon: Cloud,
description: 'Store backups in Amazon S3 or compatible object storage',
fields: ['backup_s3_endpoint', 'backup_s3_bucket', 'backup_s3_access_key', 'backup_s3_secret_key', 'backup_s3_region']
}
];
const scheduleOptions = [
{ value: 'hourly', label: 'Every hour' },
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly' },
{ value: 'custom', label: 'Custom cron expression' }
];
export const BackupConfiguration = ({ config, onSave, isSaving }) => {
const { t } = useTranslation();
const destinationTypes = [
{
id: 'local',
name: t('backup.configuration.destinationTypes.local.name'),
icon: HardDrive,
description: t('backup.configuration.destinationTypes.local.description'),
fields: ['backup_destination_path']
},
{
id: 'rsync',
name: t('backup.configuration.destinationTypes.rsync.name'),
icon: Server,
description: t('backup.configuration.destinationTypes.rsync.description'),
fields: ['backup_rsync_host', 'backup_rsync_user', 'backup_rsync_path', 'backup_rsync_ssh_key']
},
{
id: 's3',
name: t('backup.configuration.destinationTypes.s3.name'),
icon: Cloud,
description: t('backup.configuration.destinationTypes.s3.description'),
fields: ['backup_s3_endpoint', 'backup_s3_bucket', 'backup_s3_access_key', 'backup_s3_secret_key', 'backup_s3_region']
}
];
const scheduleOptions = [
{ value: 'hourly', label: t('backup.configuration.schedule.options.hourly') },
{ value: 'daily', label: t('backup.configuration.schedule.options.daily') },
{ value: 'weekly', label: t('backup.configuration.schedule.options.weekly') },
{ value: 'custom', label: t('backup.configuration.schedule.options.custom') }
];
const [formData, setFormData] = useState({
backup_enabled: false,
backup_destination_type: 'local',
@@ -121,7 +124,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
}
if (missingFields.length > 0) {
toast.error('Please fill in all required fields');
toast.error(t('backup.configuration.messages.requiredFields'));
return;
}
@@ -133,9 +136,9 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
try {
// TODO: Implement connection test endpoint
await new Promise(resolve => setTimeout(resolve, 2000));
toast.success('Connection test successful!');
toast.success(t('backup.configuration.messages.connectionSuccess'));
} catch (error) {
toast.error('Connection test failed: ' + error.message);
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + error.message);
} finally {
setTestingConnection(false);
}
@@ -149,9 +152,9 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<Card className="p-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">Backup Service</h3>
<h3 className="text-lg font-semibold text-gray-900">{t('backup.configuration.enableBackup')}</h3>
<p className="mt-1 text-sm text-gray-600">
Enable automatic backups to protect your data
{t('backup.configuration.enableBackupHelp')}
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
@@ -168,7 +171,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Destination Configuration */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Backup Destination</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.destinationType')}</h3>
{/* Destination Type Selection */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
@@ -203,17 +206,17 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Backup Directory Path
{t('backup.configuration.fields.destinationPath')}
</label>
<Input
type="text"
value={formData.backup_destination_path}
onChange={(e) => handleChange('backup_destination_path', e.target.value)}
placeholder="/path/to/backup/directory"
placeholder={t('backup.configuration.fields.destinationPathPlaceholder')}
required
/>
<p className="mt-1 text-xs text-gray-500">
Absolute path where backups will be stored
{t('backup.configuration.fields.destinationPathHelp')}
</p>
</div>
</>
@@ -224,50 +227,50 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
SSH Host
{t('backup.configuration.fields.rsyncHost')}
</label>
<Input
type="text"
value={formData.backup_rsync_host}
onChange={(e) => handleChange('backup_rsync_host', e.target.value)}
placeholder="backup.example.com"
placeholder={t('backup.configuration.fields.rsyncHostPlaceholder')}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
SSH User
{t('backup.configuration.fields.rsyncUser')}
</label>
<Input
type="text"
value={formData.backup_rsync_user}
onChange={(e) => handleChange('backup_rsync_user', e.target.value)}
placeholder="backup-user"
placeholder={t('backup.configuration.fields.rsyncUserPlaceholder')}
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Remote Path
{t('backup.configuration.fields.rsyncPath')}
</label>
<Input
type="text"
value={formData.backup_rsync_path}
onChange={(e) => handleChange('backup_rsync_path', e.target.value)}
placeholder="/home/backup/photo-sharing"
placeholder={t('backup.configuration.fields.rsyncPathPlaceholder')}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
SSH Private Key (optional)
{t('backup.configuration.fields.rsyncSshKey')}
</label>
<div className="relative">
<textarea
value={formData.backup_rsync_ssh_key}
onChange={(e) => handleChange('backup_rsync_ssh_key', e.target.value)}
placeholder="-----BEGIN RSA PRIVATE KEY-----"
placeholder={t('backup.configuration.fields.rsyncSshKeyPlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary font-mono text-sm"
rows={4}
/>
@@ -280,7 +283,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</button>
</div>
<p className="mt-1 text-xs text-gray-500">
Leave empty to use system SSH keys
{t('backup.configuration.fields.rsyncSshKeyHelp')}
</p>
</div>
</>
@@ -290,7 +293,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
S3 Endpoint URL
{t('backup.configuration.fields.s3Endpoint')}
</label>
<Input
type="text"
@@ -300,13 +303,13 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
required
/>
<p className="mt-1 text-xs text-gray-500">
Use default for AWS S3, or your provider's endpoint
{t('backup.configuration.fields.s3EndpointHelp')}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bucket Name
{t('backup.configuration.fields.s3Bucket')}
</label>
<Input
type="text"
@@ -318,7 +321,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Region
{t('backup.configuration.fields.s3Region')}
</label>
<Input
type="text"
@@ -331,7 +334,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Access Key ID
{t('backup.configuration.fields.s3AccessKey')}
</label>
<Input
type="text"
@@ -343,7 +346,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Secret Access Key
{t('backup.configuration.fields.s3SecretKey')}
</label>
<div className="relative">
<Input
@@ -379,12 +382,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{testingConnection ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Testing...
{t('backup.configuration.testingConnection')}
</>
) : (
<>
<TestTube className="mr-2 h-4 w-4" />
Test Connection
<Wifi className="mr-2 h-4 w-4" />
{t('backup.actions.testConnection')}
</>
)}
</Button>
@@ -395,12 +398,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Schedule Configuration */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Backup Schedule</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.schedule.title')}</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Schedule
{t('backup.configuration.schedule.scheduleType')}
</label>
<select
value={formData.backup_schedule}
@@ -418,7 +421,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{formData.backup_schedule === 'custom' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cron Expression
{t('backup.configuration.schedule.customCron')}
</label>
<Input
type="text"
@@ -427,14 +430,14 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
placeholder="0 3 * * *"
/>
<p className="mt-1 text-xs text-gray-500">
Use standard cron syntax (minute hour day month weekday)
{t('backup.configuration.schedule.customCronHelp')}
</p>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Retention Period (days)
{t('backup.configuration.schedule.retention')}
</label>
<Input
type="number"
@@ -444,7 +447,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
max="365"
/>
<p className="mt-1 text-xs text-gray-500">
Backups older than this will be automatically deleted
{t('backup.configuration.schedule.retentionHelp')}
</p>
</div>
</div>
@@ -452,7 +455,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Backup Content Selection */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Backup Content</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.whatToBackup.title')}</h3>
<div className="space-y-3">
<label className="flex items-center">
@@ -465,9 +468,9 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<div className="ml-3">
<div className="flex items-center space-x-2">
<Database className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">Database</span>
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.database')}</span>
</div>
<p className="text-xs text-gray-500">All application data and settings</p>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.databaseHelp')}</p>
</div>
</label>
@@ -481,9 +484,9 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<div className="ml-3">
<div className="flex items-center space-x-2">
<Image className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">Photos</span>
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.photos')}</span>
</div>
<p className="text-xs text-gray-500">All uploaded photos and galleries</p>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.photosHelp')}</p>
</div>
</label>
@@ -497,9 +500,9 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<div className="ml-3">
<div className="flex items-center space-x-2">
<FileArchive className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">Archives</span>
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.archives')}</span>
</div>
<p className="text-xs text-gray-500">Expired gallery archives</p>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.archivesHelp')}</p>
</div>
</label>
@@ -513,9 +516,9 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<div className="ml-3">
<div className="flex items-center space-x-2">
<Image className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">Thumbnails</span>
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.thumbnails')}</span>
</div>
<p className="text-xs text-gray-500">Generated thumbnail images (can be regenerated)</p>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.thumbnailsHelp')}</p>
</div>
</label>
</div>
@@ -523,7 +526,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Advanced Options */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Advanced Options</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.advancedOptions.title')}</h3>
<div className="space-y-4">
<label className="flex items-center">
@@ -534,8 +537,8 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<div className="ml-3">
<span className="text-sm font-medium text-gray-700">Enable Compression</span>
<p className="text-xs text-gray-500">Reduce backup size with gzip compression</p>
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.advancedOptions.compression')}</span>
<p className="text-xs text-gray-500">{t('backup.configuration.advancedOptions.compressionHelp')}</p>
</div>
</label>
@@ -548,22 +551,22 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<div className="ml-3">
<span className="text-sm font-medium text-gray-700">Enable Encryption</span>
<p className="text-xs text-gray-500">Encrypt backups with AES-256</p>
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.advancedOptions.encryption')}</span>
<p className="text-xs text-gray-500">{t('backup.configuration.advancedOptions.encryptionHelp')}</p>
</div>
</label>
{formData.backup_encryption && (
<div className="ml-7">
<label className="block text-sm font-medium text-gray-700 mb-1">
Encryption Passphrase
{t('backup.configuration.advancedOptions.encryptionPassphrase')}
</label>
<div className="relative">
<Input
type={showSecrets.encryption_passphrase ? 'text' : 'password'}
value={formData.backup_encryption_passphrase}
onChange={(e) => handleChange('backup_encryption_passphrase', e.target.value)}
placeholder="Enter a strong passphrase"
placeholder={t('backup.configuration.advancedOptions.encryptionPassphraseHelp')}
required={formData.backup_encryption}
/>
<button
@@ -576,7 +579,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</div>
<p className="mt-1 text-xs text-red-600">
<AlertCircle className="inline h-3 w-3 mr-1" />
Store this passphrase securely! You'll need it to restore encrypted backups.
{t('backup.configuration.advancedOptions.encryptionPassphraseHelp')}
</p>
</div>
)}
@@ -593,12 +596,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
{t('backup.configuration.savingSettings')}
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Save Configuration
{t('backup.configuration.saveSettings')}
</>
)}
</Button>
@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
HardDrive,
Database,
@@ -46,6 +47,7 @@ const formatBytes = (bytes) => {
};
export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }) => {
const { t } = useTranslation();
const lastBackup = status?.lastBackup;
const statistics = lastBackup?.statistics || {};
const isConfigured = config && config.backup_destination_type;
@@ -53,22 +55,22 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
// Calculate backup health score
const getHealthScore = () => {
if (!lastBackup) return { score: 0, status: 'critical', message: 'No backups found' };
if (!lastBackup) return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.noBackups') };
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' };
return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.lastBackupFailed') };
}
if (hoursSinceBackup < 24) {
return { score: 100, status: 'excellent', message: 'Backup is up to date' };
return { score: 100, status: 'excellent', message: t('backup.dashboard.healthMessages.upToDate') };
} else if (hoursSinceBackup < 48) {
return { score: 75, status: 'good', message: 'Backup is recent' };
return { score: 75, status: 'good', message: t('backup.dashboard.healthMessages.recent') };
} else if (hoursSinceBackup < 168) { // 1 week
return { score: 50, status: 'warning', message: 'Backup is getting old' };
return { score: 50, status: 'warning', message: t('backup.dashboard.healthMessages.gettingOld') };
} else {
return { score: 25, status: 'critical', message: 'Backup is outdated' };
return { score: 25, status: 'critical', message: t('backup.dashboard.healthMessages.outdated') };
}
};
@@ -89,10 +91,10 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<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
{t('backup.dashboard.notConfigured.title')}
</h3>
<p className="mt-1 text-sm text-amber-700">
Please configure backup settings in the Configuration tab before running backups.
{t('backup.dashboard.notConfigured.message')}
</p>
</div>
</div>
@@ -102,7 +104,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{/* 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>
<h3 className="text-lg font-semibold text-gray-900">{t('backup.dashboard.health.title')}</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>
@@ -153,12 +155,12 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{isBackupRunning ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Running...
{t('backup.dashboard.actions.running')}
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Run Backup Now
{t('backup.dashboard.actions.runBackupNow')}
</>
)}
</Button>
@@ -170,23 +172,23 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
icon={FileArchive}
label="Total Backups"
label={t('backup.dashboard.stats.totalBackups')}
value={status?.totalBackups || 0}
color="blue"
subtext={lastBackup ? `Last: ${format(new Date(lastBackup.created_at), 'PP')}` : 'No backups yet'}
subtext={lastBackup ? `${t('backup.dashboard.stats.last')}: ${format(new Date(lastBackup.created_at), 'PP')}` : t('backup.dashboard.stats.noBackupsYet')}
/>
<StatCard
icon={HardDrive}
label="Backup Size"
label={t('backup.dashboard.stats.backupSize')}
value={formatBytes(statistics.total_size || 0)}
color="green"
subtext={`${statistics.files_processed || 0} files`}
subtext={`${statistics.files_processed || 0} ${t('backup.dashboard.stats.files')}`}
/>
<StatCard
icon={Clock}
label="Last Duration"
label={t('backup.dashboard.stats.lastDuration')}
value={lastBackup ? `${Math.round(lastBackup.duration_seconds / 60)}m` : 'N/A'}
color="purple"
subtext={lastBackup ? format(new Date(lastBackup.created_at), 'p') : ''}
@@ -194,17 +196,17 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<StatCard
icon={Shield}
label="Backup Status"
value={isEnabled ? 'Active' : 'Inactive'}
label={t('backup.dashboard.stats.backupStatus')}
value={isEnabled ? t('backup.dashboard.stats.active') : t('backup.dashboard.stats.inactive')}
color={isEnabled ? 'green' : 'gray'}
subtext={config?.backup_destination_type || 'Not configured'}
subtext={config?.backup_destination_type || t('backup.dashboard.notConfigured.title')}
/>
</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>
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.dashboard.recentActivity.title')}</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">
@@ -218,7 +220,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
)}
<div>
<p className="font-medium text-gray-900">
{backup.backup_type} backup
{t('backup.dashboard.backupType', { type: backup.backup_type })}
</p>
<p className="text-sm text-gray-500">
{format(new Date(backup.created_at), 'PPp')}
@@ -242,7 +244,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{/* 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>
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.dashboard.coverage.title')}</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
@@ -252,34 +254,34 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<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'}
{statistics.database_backed_up ? t('backup.dashboard.coverage.included') : t('backup.dashboard.coverage.excluded')}
</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>
<span className="text-gray-700">{t('backup.configuration.whatToBackup.photos')}</span>
</div>
<span className="text-sm text-gray-500">
{statistics.photos_backed_up || 0} of {statistics.total_photos || 0}
{statistics.photos_backed_up || 0} {t('common.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>
<span className="text-gray-700">{t('backup.configuration.whatToBackup.archives')}</span>
</div>
<span className="text-sm text-gray-500">
{statistics.archives_backed_up || 0} files
{statistics.archives_backed_up || 0} {t('backup.dashboard.stats.files')}
</span>
</div>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Destination Info</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.dashboard.storageDestination')}</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3">
{config?.backup_destination_type === 's3' ? (
@@ -292,8 +294,8 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<div>
<p className="font-medium text-gray-900">
{config?.backup_destination_type
? config.backup_destination_type.toUpperCase()
: 'Not Configured'}
? t(`backup.configuration.destinationTypes.${config.backup_destination_type}.name`)
: t('backup.dashboard.notConfigured.title')}
</p>
<p className="text-sm text-gray-500">
{config?.backup_destination_type === 's3' && config?.backup_s3_bucket
@@ -302,7 +304,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
? `Path: ${config.backup_destination_path}`
: config?.backup_destination_type === 'rsync' && config?.backup_rsync_host
? `Host: ${config.backup_rsync_host}`
: 'No destination set'}
: t('backup.dashboard.noDestinationSet')}
</p>
</div>
</div>
@@ -312,7 +314,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<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
{t('backup.configuration.schedule.retentionDays')} {config.backup_retention_days} {t('backup.configuration.schedule.retentionHelp').replace('days (older backups will be automatically deleted)', '')}
</span>
</div>
</div>
+30 -28
View File
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Download,
Eye,
@@ -41,6 +42,7 @@ const formatBytes = (bytes) => {
};
export const BackupHistory = () => {
const { t } = useTranslation();
const [expandedRows, setExpandedRows] = useState(new Set());
const [searchTerm, setSearchTerm] = useState('');
const [filterStatus, setFilterStatus] = useState('all');
@@ -110,7 +112,7 @@ export const BackupHistory = () => {
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
type="text"
placeholder="Search backups..."
placeholder={t('backup.history.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
@@ -149,22 +151,22 @@ export const BackupHistory = () => {
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
{t('backup.history.columns.status')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date & Time
{t('backup.history.columns.dateTime')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
{t('backup.history.columns.type')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Size
{t('backup.history.columns.size')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Duration
{t('backup.history.columns.duration')}
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
{t('backup.history.columns.actions')}
</th>
</tr>
</thead>
@@ -215,7 +217,7 @@ export const BackupHistory = () => {
{formatBytes(stats.total_size || 0)}
</p>
<p className="text-xs text-gray-500">
{stats.files_processed || 0} files
{stats.files_processed || 0} {t('backup.dashboard.stats.files')}
</p>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
@@ -228,7 +230,7 @@ export const BackupHistory = () => {
<button
onClick={() => toggleRowExpansion(backup.id)}
className="text-gray-400 hover:text-gray-600"
title="View details"
title={t('backup.actions.view')}
>
{isExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</button>
@@ -236,7 +238,7 @@ export const BackupHistory = () => {
<button
onClick={() => window.open(`/admin/backup/download/${backup.id}`, '_blank')}
className="text-gray-400 hover:text-gray-600"
title="Download backup"
title={t('backup.actions.download')}
>
<Download size={20} />
</button>
@@ -244,7 +246,7 @@ export const BackupHistory = () => {
<button
onClick={() => handleDelete(backup)}
className="text-gray-400 hover:text-red-600"
title="Delete backup"
title={t('backup.actions.delete')}
disabled={deleteMutation.isLoading}
>
<Trash2 size={20} />
@@ -260,19 +262,19 @@ export const BackupHistory = () => {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Backup Details */}
<div className="space-y-2">
<h4 className="font-medium text-gray-900">Backup Details</h4>
<h4 className="font-medium text-gray-900">{t('backup.history.details.backupDetails')}</h4>
<div className="text-sm space-y-1">
<div className="flex justify-between">
<span className="text-gray-500">Destination:</span>
<span className="text-gray-500">{t('backup.history.details.destination')}:</span>
<span className="text-gray-900">{backup.destination_type || 'Unknown'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Started:</span>
<span className="text-gray-500">{t('backup.history.details.started')}:</span>
<span className="text-gray-900">{format(new Date(backup.created_at), 'p')}</span>
</div>
{backup.completed_at && (
<div className="flex justify-between">
<span className="text-gray-500">Completed:</span>
<span className="text-gray-500">{t('backup.history.details.completed')}:</span>
<span className="text-gray-900">{format(new Date(backup.completed_at), 'p')}</span>
</div>
)}
@@ -281,11 +283,11 @@ export const BackupHistory = () => {
{/* Content Backed Up */}
<div className="space-y-2">
<h4 className="font-medium text-gray-900">Content Backed Up</h4>
<h4 className="font-medium text-gray-900">{t('backup.history.details.contentBackedUp')}</h4>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Database className={`h-4 w-4 ${stats.database_backed_up ? 'text-green-500' : 'text-gray-300'}`} />
<span className="text-sm text-gray-700">Database</span>
<span className="text-sm text-gray-700">{t('backup.configuration.whatToBackup.database')}</span>
</div>
<div className="flex items-center space-x-2">
<Image className={`h-4 w-4 ${stats.photos_backed_up > 0 ? 'text-green-500' : 'text-gray-300'}`} />
@@ -305,7 +307,7 @@ export const BackupHistory = () => {
{/* Error Information */}
{backup.error_message && (
<div className="space-y-2">
<h4 className="font-medium text-red-900">Error Details</h4>
<h4 className="font-medium text-red-900">{t('backup.history.details.errorDetails')}</h4>
<p className="text-sm text-red-700 bg-red-50 p-2 rounded">
{backup.error_message}
</p>
@@ -315,7 +317,7 @@ export const BackupHistory = () => {
{/* Manifest Path */}
{backup.manifest_path && (
<div className="space-y-2">
<h4 className="font-medium text-gray-900">Manifest</h4>
<h4 className="font-medium text-gray-900">{t('backup.history.details.manifest')}</h4>
<p className="text-sm text-gray-600 font-mono break-all">
{backup.manifest_path}
</p>
@@ -344,7 +346,7 @@ export const BackupHistory = () => {
variant="secondary"
size="sm"
>
Previous
{t('backup.history.pagination.previous')}
</Button>
<Button
onClick={() => setCurrentPage(p => Math.min(pagination.pages, p + 1))}
@@ -352,17 +354,17 @@ export const BackupHistory = () => {
variant="secondary"
size="sm"
>
Next
{t('backup.history.pagination.next')}
</Button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{(currentPage - 1) * pagination.limit + 1}</span> to{' '}
<span className="font-medium">
{Math.min(currentPage * pagination.limit, pagination.total)}
</span>{' '}
of <span className="font-medium">{pagination.total}</span> results
{t('backup.history.pagination.showing', {
from: (currentPage - 1) * pagination.limit + 1,
to: Math.min(currentPage * pagination.limit, pagination.total),
total: pagination.total
})}
</p>
</div>
<div>
@@ -372,7 +374,7 @@ export const BackupHistory = () => {
disabled={currentPage === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
{t('backup.history.pagination.previous')}
</button>
{[...Array(Math.min(5, pagination.pages))].map((_, i) => {
@@ -397,7 +399,7 @@ export const BackupHistory = () => {
disabled={currentPage === pagination.pages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
{t('backup.history.pagination.next')}
</button>
</nav>
</div>
+103 -101
View File
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
RefreshCw,
AlertTriangle,
@@ -28,47 +29,49 @@ import { useQuery, useMutation } from '@tanstack/react-query';
import { Button, Card, Input, Loading } from '../common';
import { api } from '../../config/api';
const steps = [
{ id: 'source', title: 'Select Source' },
{ id: 'backup', title: 'Choose Backup' },
{ id: 'options', title: 'Restore Options' },
{ id: 'confirm', title: 'Review & Confirm' },
{ id: 'progress', title: 'Restore Progress' }
];
const restoreTypes = [
{
id: 'full',
name: 'Full Restore',
description: 'Restore everything including database, photos, and archives',
icon: RefreshCw,
warning: 'This will replace all current data'
},
{
id: 'database',
name: 'Database Only',
description: 'Restore only the database (settings, events, users)',
icon: Database,
warning: 'Current database will be replaced'
},
{
id: 'files',
name: 'Files Only',
description: 'Restore only photos and archives',
icon: Image,
warning: 'Existing files may be overwritten'
},
{
id: 'selective',
name: 'Selective Restore',
description: 'Choose specific items to restore',
icon: CheckCircle,
warning: 'Only selected items will be restored'
}
];
export const RestoreWizard = () => {
const { t } = useTranslation();
const [currentStep, setCurrentStep] = useState(0);
const steps = [
{ id: 'source', title: t('backup.restore.steps.selectSource') },
{ id: 'backup', title: t('backup.restore.steps.chooseBackup') },
{ id: 'options', title: t('backup.restore.steps.restoreOptions') },
{ id: 'confirm', title: t('backup.restore.steps.reviewConfirm') },
{ id: 'progress', title: t('backup.restore.steps.restoreProgress') }
];
const restoreTypes = [
{
id: 'full',
name: t('backup.restore.restoreTypes.full.name'),
description: t('backup.restore.restoreTypes.full.description'),
icon: RefreshCw,
warning: t('backup.restore.restoreTypes.full.warning')
},
{
id: 'database',
name: t('backup.restore.restoreTypes.database.name'),
description: t('backup.restore.restoreTypes.database.description'),
icon: Database,
warning: t('backup.restore.restoreTypes.database.warning')
},
{
id: 'files',
name: t('backup.restore.restoreTypes.files.name'),
description: t('backup.restore.restoreTypes.files.description'),
icon: Image,
warning: t('backup.restore.restoreTypes.files.warning')
},
{
id: 'selective',
name: t('backup.restore.restoreTypes.selective.name'),
description: t('backup.restore.restoreTypes.selective.description'),
icon: CheckCircle,
warning: t('backup.restore.restoreTypes.selective.warning')
}
];
const [restoreData, setRestoreData] = useState({
source: null,
sourceConfig: {},
@@ -184,8 +187,8 @@ export const RestoreWizard = () => {
const renderSourceSelection = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Select Backup Source</h3>
<p className="text-sm text-gray-600">Choose where to restore the backup from</p>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.source.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.source.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -200,8 +203,8 @@ export const RestoreWizard = () => {
<HardDrive className={`h-12 w-12 mb-3 mx-auto ${
restoreData.source === 'local' ? 'text-primary' : 'text-gray-400'
}`} />
<h4 className="font-medium text-gray-900">Local Backup</h4>
<p className="text-xs text-gray-500 mt-1">Restore from local filesystem</p>
<h4 className="font-medium text-gray-900">{t('backup.restore.source.local.name')}</h4>
<p className="text-xs text-gray-500 mt-1">{t('backup.restore.source.local.description')}</p>
</button>
<button
@@ -215,8 +218,8 @@ export const RestoreWizard = () => {
<Cloud className={`h-12 w-12 mb-3 mx-auto ${
restoreData.source === 's3' ? 'text-primary' : 'text-gray-400'
}`} />
<h4 className="font-medium text-gray-900">S3 Storage</h4>
<p className="text-xs text-gray-500 mt-1">Restore from S3 bucket</p>
<h4 className="font-medium text-gray-900">{t('backup.restore.source.s3.name')}</h4>
<p className="text-xs text-gray-500 mt-1">{t('backup.restore.source.s3.description')}</p>
</button>
<button
@@ -230,18 +233,18 @@ export const RestoreWizard = () => {
<Upload className={`h-12 w-12 mb-3 mx-auto ${
restoreData.source === 'upload' ? 'text-primary' : 'text-gray-400'
}`} />
<h4 className="font-medium text-gray-900">Upload Backup</h4>
<p className="text-xs text-gray-500 mt-1">Upload a backup file</p>
<h4 className="font-medium text-gray-900">{t('backup.restore.source.upload.name')}</h4>
<p className="text-xs text-gray-500 mt-1">{t('backup.restore.source.upload.description')}</p>
</button>
</div>
{/* Source-specific configuration */}
{restoreData.source === 's3' && (
<Card className="p-4 space-y-4">
<h4 className="font-medium text-gray-900">S3 Configuration</h4>
<h4 className="font-medium text-gray-900">{t('backup.restore.source.configuration.s3')}</h4>
<div className="grid grid-cols-2 gap-4">
<Input
placeholder="S3 Endpoint URL"
placeholder={t('backup.restore.source.configuration.endpoint')}
value={restoreData.sourceConfig.s3Endpoint || ''}
onChange={(e) => setRestoreData(prev => ({
...prev,
@@ -249,7 +252,7 @@ export const RestoreWizard = () => {
}))}
/>
<Input
placeholder="Bucket Name"
placeholder={t('backup.restore.source.configuration.bucket')}
value={restoreData.sourceConfig.s3Bucket || ''}
onChange={(e) => setRestoreData(prev => ({
...prev,
@@ -257,7 +260,7 @@ export const RestoreWizard = () => {
}))}
/>
<Input
placeholder="Access Key ID"
placeholder={t('backup.restore.source.configuration.accessKey')}
value={restoreData.sourceConfig.s3AccessKey || ''}
onChange={(e) => setRestoreData(prev => ({
...prev,
@@ -266,7 +269,7 @@ export const RestoreWizard = () => {
/>
<Input
type="password"
placeholder="Secret Access Key"
placeholder={t('backup.restore.source.configuration.secretKey')}
value={restoreData.sourceConfig.s3SecretKey || ''}
onChange={(e) => setRestoreData(prev => ({
...prev,
@@ -281,7 +284,7 @@ export const RestoreWizard = () => {
<Card className="p-4">
<div className="text-center py-8">
<Upload className="h-12 w-12 mx-auto mb-3 text-gray-400" />
<p className="text-sm text-gray-600">Upload functionality coming soon</p>
<p className="text-sm text-gray-600">{t('backup.restore.source.upload.comingSoon')}</p>
</div>
</Card>
)}
@@ -291,8 +294,8 @@ export const RestoreWizard = () => {
const renderBackupSelection = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Choose Backup to Restore</h3>
<p className="text-sm text-gray-600">Select from available backups</p>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.backup.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.backup.subtitle')}</p>
</div>
{loadingBackups ? (
@@ -300,7 +303,7 @@ export const RestoreWizard = () => {
) : availableBackups?.length === 0 ? (
<Card className="p-8 text-center">
<FileArchive className="h-12 w-12 mx-auto mb-3 text-gray-300" />
<p className="text-gray-500">No backups found in selected source</p>
<p className="text-gray-500">{t('backup.restore.backup.noBackupsFound')}</p>
</Card>
) : (
<div className="space-y-3">
@@ -327,10 +330,10 @@ export const RestoreWizard = () => {
</div>
<div>
<p className="font-medium text-gray-900">
{format(new Date(backup.created_at), 'PPP')} at {format(new Date(backup.created_at), 'p')}
{format(new Date(backup.created_at), 'PPP')} {t('backup.restore.backup.at')} {format(new Date(backup.created_at), 'p')}
</p>
<p className="text-sm text-gray-500">
{backup.backup_type} backup {formatBytes(backup.total_size || 0)}
{t('backup.dashboard.backupType', { type: backup.backup_type })} {formatBytes(backup.total_size || 0)}
</p>
</div>
</div>
@@ -348,13 +351,13 @@ export const RestoreWizard = () => {
<div className="flex items-start space-x-3">
<Shield className="h-5 w-5 text-amber-600 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-amber-900">Encrypted Backup</p>
<p className="text-sm font-medium text-amber-900">{t('backup.restore.backup.encrypted')}</p>
<p className="text-sm text-amber-700 mt-1">
You'll need to provide the encryption passphrase to restore this backup.
{t('backup.restore.backup.encryptedMessage')}
</p>
<Input
type="password"
placeholder="Enter encryption passphrase"
placeholder={t('backup.restore.backup.enterPassphrase')}
className="mt-3"
value={restoreData.encryptionPassphrase}
onChange={(e) => setRestoreData(prev => ({
@@ -372,8 +375,8 @@ export const RestoreWizard = () => {
const renderRestoreOptions = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Restore Options</h3>
<p className="text-sm text-gray-600">Choose what to restore</p>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.options.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.options.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -409,7 +412,7 @@ export const RestoreWizard = () => {
{/* Additional Options */}
<Card className="p-4 space-y-4">
<h4 className="font-medium text-gray-900">Additional Options</h4>
<h4 className="font-medium text-gray-900">{t('backup.restore.options.additionalOptions.title')}</h4>
<label className="flex items-start space-x-3">
<input
@@ -422,9 +425,9 @@ export const RestoreWizard = () => {
className="mt-1 h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<div>
<p className="text-sm font-medium text-gray-700">Skip Pre-Restore Backup</p>
<p className="text-sm font-medium text-gray-700">{t('backup.restore.options.additionalOptions.skipPreBackup')}</p>
<p className="text-xs text-gray-500">
By default, a backup is created before restore. Check this to skip it.
{t('backup.restore.options.additionalOptions.skipPreBackupHelp')}
</p>
</div>
</label>
@@ -440,9 +443,9 @@ export const RestoreWizard = () => {
className="mt-1 h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<div>
<p className="text-sm font-medium text-gray-700">Force Restore</p>
<p className="text-sm font-medium text-gray-700">{t('backup.restore.options.additionalOptions.force')}</p>
<p className="text-xs text-gray-500">
Override safety checks and warnings (use with caution)
{t('backup.restore.options.additionalOptions.forceHelp')}
</p>
</div>
</label>
@@ -453,8 +456,8 @@ export const RestoreWizard = () => {
const renderConfirmation = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Review & Confirm</h3>
<p className="text-sm text-gray-600">Please review your restore configuration</p>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.confirmation.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.confirmation.subtitle')}</p>
</div>
{validationResult ? (
@@ -476,8 +479,8 @@ export const RestoreWizard = () => {
validationResult.validation?.isValid ? 'text-green-900' : 'text-red-900'
}`}>
{validationResult.validation?.isValid
? 'Validation Passed'
: 'Validation Failed'}
? t('backup.restore.confirmation.validation.passed')
: t('backup.restore.confirmation.validation.failed')}
</p>
{validationResult.validation?.errors?.length > 0 && (
<ul className="mt-2 text-sm text-red-700 list-disc list-inside">
@@ -493,20 +496,20 @@ export const RestoreWizard = () => {
{/* Space Check */}
{validationResult.spaceCheck && (
<Card className="p-4">
<h4 className="font-medium text-gray-900 mb-3">Storage Space</h4>
<h4 className="font-medium text-gray-900 mb-3">{t('backup.restore.confirmation.spaceCheck.title')}</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Required:</span>
<span className="text-gray-600">{t('backup.restore.confirmation.spaceCheck.required')}:</span>
<span className="font-medium">{formatBytes(validationResult.spaceCheck.required)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Available:</span>
<span className="text-gray-600">{t('backup.restore.confirmation.spaceCheck.available')}:</span>
<span className="font-medium">{formatBytes(validationResult.spaceCheck.available)}</span>
</div>
{!validationResult.spaceCheck.sufficient && (
<p className="text-red-600 text-xs mt-2">
<AlertCircle className="inline h-3 w-3 mr-1" />
Insufficient storage space
{t('backup.restore.confirmation.spaceCheck.insufficient')}
</p>
)}
</div>
@@ -515,25 +518,25 @@ export const RestoreWizard = () => {
{/* Summary */}
<Card className="p-4">
<h4 className="font-medium text-gray-900 mb-3">Restore Summary</h4>
<h4 className="font-medium text-gray-900 mb-3">{t('backup.restore.confirmation.summary.title')}</h4>
<dl className="space-y-2 text-sm">
<div className="flex justify-between">
<dt className="text-gray-600">Source:</dt>
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.source')}:</dt>
<dd className="font-medium capitalize">{restoreData.source}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600">Backup Date:</dt>
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.backupDate')}:</dt>
<dd className="font-medium">
{format(new Date(restoreData.selectedBackup.created_at), 'PPp')}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600">Restore Type:</dt>
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.restoreType')}:</dt>
<dd className="font-medium capitalize">{restoreData.restoreType}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600">Pre-backup:</dt>
<dd className="font-medium">{restoreData.skipPreBackup ? 'Skipped' : 'Enabled'}</dd>
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.preBackup')}:</dt>
<dd className="font-medium">{restoreData.skipPreBackup ? t('backup.restore.confirmation.summary.skipped') : t('backup.restore.confirmation.summary.enabled')}</dd>
</div>
</dl>
</Card>
@@ -544,11 +547,10 @@ export const RestoreWizard = () => {
<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">
Important Notice
{t('backup.restore.confirmation.warning.title')}
</h3>
<p className="mt-1 text-sm text-amber-700">
This restore operation will replace existing data. Make sure you have a current backup
before proceeding. This action cannot be undone.
{t('backup.restore.confirmation.warning.message')}
</p>
</div>
</div>
@@ -557,7 +559,7 @@ export const RestoreWizard = () => {
) : (
<div className="text-center py-8">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
<p className="mt-2 text-sm text-gray-600">Validating restore configuration...</p>
<p className="mt-2 text-sm text-gray-600">{t('backup.restore.confirmation.validation.checking')}</p>
</div>
)}
</div>
@@ -570,9 +572,9 @@ export const RestoreWizard = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Restore Progress</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.progress.title')}</h3>
<p className="text-sm text-gray-600">
{isRunning ? 'Restore in progress...' : 'Restore completed'}
{isRunning ? t('backup.restore.progress.inProgress') : t('backup.restore.progress.completed')}
</p>
</div>
@@ -580,7 +582,7 @@ export const RestoreWizard = () => {
<Card className="p-6">
<div className="space-y-4">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Overall Progress</span>
<span className="text-gray-600">{t('backup.restore.progress.overallProgress')}</span>
<span className="font-medium">{progress.percentage || 0}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
@@ -591,7 +593,7 @@ export const RestoreWizard = () => {
</div>
{progress.currentFile && (
<p className="text-sm text-gray-600">
Current: {progress.currentFile}
{t('backup.restore.progress.current')}: {progress.currentFile}
</p>
)}
</div>
@@ -599,7 +601,7 @@ export const RestoreWizard = () => {
{/* Status Details */}
<Card className="p-6">
<h4 className="font-medium text-gray-900 mb-4">Status Details</h4>
<h4 className="font-medium text-gray-900 mb-4">{t('backup.restore.progress.statusDetails')}</h4>
<div className="space-y-3">
{progress.steps?.map((step, idx) => (
<div key={idx} className="flex items-center space-x-3">
@@ -629,7 +631,7 @@ export const RestoreWizard = () => {
{/* Logs */}
{progress.logs && progress.logs.length > 0 && (
<Card className="p-6">
<h4 className="font-medium text-gray-900 mb-4">Restore Logs</h4>
<h4 className="font-medium text-gray-900 mb-4">{t('backup.restore.progress.restoreLogs')}</h4>
<div className="bg-gray-900 rounded-lg p-4 max-h-64 overflow-y-auto">
<pre className="text-xs text-gray-300 font-mono">
{progress.logs.join('\n')}
@@ -645,10 +647,10 @@ export const RestoreWizard = () => {
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
<div className="ml-3">
<h3 className="text-sm font-medium text-green-800">
Restore Completed Successfully
{t('backup.restore.progress.success.title')}
</h3>
<p className="mt-1 text-sm text-green-700">
Your data has been restored. Please verify everything is working correctly.
{t('backup.restore.progress.success.message')}
</p>
</div>
</div>
@@ -726,7 +728,7 @@ export const RestoreWizard = () => {
disabled={currentStep === 0 || currentStep === 4}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Back
{t('backup.restore.actions.back')}
</Button>
{currentStep < 4 && (
@@ -739,12 +741,12 @@ export const RestoreWizard = () => {
{restoreMutation.isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Starting...
{t('backup.restore.actions.starting')}
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Start Restore
{t('backup.restore.actions.startRestore')}
</>
)}
</>
@@ -753,18 +755,18 @@ export const RestoreWizard = () => {
{validateMutation.isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Validating...
{t('backup.restore.actions.validating')}
</>
) : (
<>
Next
{t('backup.restore.actions.next')}
<ChevronRight className="ml-2 h-4 w-4" />
</>
)}
</>
) : (
<>
Next
{t('backup.restore.actions.next')}
<ChevronRight className="ml-2 h-4 w-4" />
</>
)}
@@ -788,7 +790,7 @@ export const RestoreWizard = () => {
setValidationResult(null);
}}
>
Start New Restore
{t('backup.restore.actions.startNewRestore')}
</Button>
)}
</div>
+398
View File
@@ -903,6 +903,404 @@
"passwordSecurityRequirements": "Passwort erfüllt nicht die Sicherheitsanforderungen",
"expirationRange": "Ablauf muss zwischen 1 und 365 Tagen liegen"
},
"backup": {
"title": "Backup-Verwaltung",
"subtitle": "Verwalten Sie System-Backups, konfigurieren Sie automatisierte Backups und stellen Sie vorherige Backups wieder her.",
"tabs": {
"dashboard": "Dashboard",
"configuration": "Konfiguration",
"history": "Backup-Verlauf",
"restore": "Wiederherstellung"
},
"status": {
"inProgress": "Backup läuft...",
"lastBackup": "Letztes Backup",
"noBackups": "Keine Backups gefunden",
"nextBackup": "Nächstes Backup",
"notScheduled": "Nicht geplant",
"enabled": "Aktiviert",
"disabled": "Deaktiviert"
},
"actions": {
"runBackupNow": "Backup jetzt starten",
"starting": "Starte...",
"running": "Läuft...",
"testConnection": "Verbindung testen",
"save": "Konfiguration speichern",
"delete": "Löschen",
"view": "Details anzeigen",
"download": "Herunterladen",
"refresh": "Aktualisieren"
},
"dashboard": {
"backupHealth": "Backup-Status",
"healthStatus": {
"excellent": "Ausgezeichnet",
"good": "Gut",
"warning": "Warnung",
"critical": "Kritisch"
},
"healthMessages": {
"noBackups": "Keine Backups gefunden",
"failed": "Letztes Backup fehlgeschlagen",
"upToDate": "Backup ist aktuell",
"recent": "Backup ist aktuell",
"old": "Backup wird alt",
"outdated": "Backup ist veraltet"
},
"stats": {
"totalBackups": "Gesamt-Backups",
"backupSize": "Backup-Größe",
"lastDuration": "Letzte Dauer",
"backupStatus": "Backup-Status",
"last": "Letztes",
"files": "Dateien",
"minutes": "{{count}}m",
"active": "Aktiv",
"inactive": "Inaktiv"
},
"recentActivity": {
"title": "Letzte Backup-Aktivitäten"
},
"notConfigured": {
"title": "Backup nicht konfiguriert",
"message": "Bitte konfigurieren Sie die Backup-Einstellungen im Konfiguration-Tab, bevor Sie Backups ausführen."
},
"coverage": {
"database": "Datenbank",
"photos": "Fotos",
"archives": "Archive",
"systemFiles": "Systemdateien",
"included": "Enthalten",
"excluded": "Ausgeschlossen",
"optional": "Optional"
},
"storageDestination": "Speicherziel",
"nextScheduledBackup": "Nächstes geplantes Backup",
"backupType": "{{type}} Backup",
"noDestinationSet": "Kein Ziel festgelegt"
},
"configuration": {
"enableBackup": "Automatisierte Backups aktivieren",
"destinationType": "Backup-Ziel",
"destinationTypes": {
"local": {
"name": "Lokaler Speicher",
"description": "Backups auf dem lokalen Server-Dateisystem speichern"
},
"rsync": {
"name": "Remote-Server (Rsync)",
"description": "Backups über SSH/Rsync auf einen Remote-Server synchronisieren"
},
"s3": {
"name": "S3-kompatibler Speicher",
"description": "Backups in Amazon S3 oder kompatiblem Objektspeicher speichern"
}
},
"fields": {
"destinationPath": "Zielpfad",
"destinationPathHelp": "Lokaler Verzeichnispfad für Backup-Speicherung",
"destinationPathPlaceholder": "/pfad/zum/backup/verzeichnis",
"rsyncHost": "Remote-Host",
"rsyncHostHelp": "SSH-Hostname oder IP-Adresse",
"rsyncHostPlaceholder": "backup.beispiel.de",
"rsyncUser": "SSH-Benutzer",
"rsyncUserHelp": "Benutzername für SSH-Verbindung",
"rsyncUserPlaceholder": "backup-benutzer",
"rsyncPath": "Remote-Pfad",
"rsyncPathHelp": "Verzeichnispfad auf Remote-Server",
"rsyncPathPlaceholder": "/home/backup/foto-sharing",
"rsyncSshKey": "SSH Private Key",
"rsyncSshKeyHelp": "SSH Private Key für Authentifizierung (optional)",
"rsyncSshKeyPlaceholder": "-----BEGIN RSA PRIVATE KEY-----",
"s3Endpoint": "S3-Endpunkt",
"s3EndpointHelp": "S3 API-Endpunkt (z.B. s3.amazonaws.com)",
"s3EndpointPlaceholder": "https://s3.amazonaws.com",
"s3Bucket": "Bucket-Name",
"s3BucketHelp": "S3-Bucket für Backup-Speicherung",
"s3BucketPlaceholder": "mein-backup-bucket",
"s3AccessKey": "Zugriffsschlüssel-ID",
"s3AccessKeyHelp": "AWS/S3 Zugriffsschlüssel-ID",
"s3AccessKeyPlaceholder": "AKIAIOSFODNN7EXAMPLE",
"s3SecretKey": "Geheimer Zugriffsschlüssel",
"s3SecretKeyHelp": "AWS/S3 geheimer Zugriffsschlüssel",
"s3SecretKeyPlaceholder": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"s3Region": "Region",
"s3RegionHelp": "S3-Region (z.B. eu-central-1)",
"s3RegionPlaceholder": "eu-central-1"
},
"schedule": {
"title": "Backup-Zeitplan",
"scheduleType": "Zeitplan-Typ",
"scheduleOptions": {
"hourly": "Jede Stunde",
"daily": "Täglich",
"weekly": "Wöchentlich",
"custom": "Benutzerdefinierter Cron-Ausdruck"
},
"options": {
"hourly": "Jede Stunde",
"daily": "Täglich",
"weekly": "Wöchentlich",
"custom": "Benutzerdefinierter Cron-Ausdruck"
},
"customCron": "Cron-Ausdruck",
"customCronHelp": "Geben Sie einen gültigen Cron-Ausdruck ein (z.B. 0 3 * * *)",
"retention": "Aufbewahrungszeitraum",
"retentionDays": "Backups aufbewahren für",
"retentionHelp": "Tage (ältere Backups werden automatisch gelöscht)"
},
"whatToBackup": {
"title": "Was soll gesichert werden",
"database": "Datenbank",
"databaseHelp": "Alle Veranstaltungsdaten, Einstellungen und Konfigurationen",
"photos": "Fotos",
"photosHelp": "Alle hochgeladenen Fotos in aktiven Galerien",
"archives": "Archive",
"archivesHelp": "Archivierte Veranstaltungs-ZIP-Dateien",
"thumbnails": "Miniaturbilder",
"thumbnailsHelp": "Generierte Miniaturbilder (können neu erstellt werden)",
"tempFiles": "Temporäre Dateien",
"tempFilesHelp": "Temporäre Upload- und Verarbeitungsdateien"
},
"advancedOptions": {
"title": "Erweiterte Optionen",
"compression": "Komprimierung aktivieren",
"compressionHelp": "Backup-Dateien komprimieren, um Speicherplatz zu sparen",
"encryption": "Verschlüsselung aktivieren",
"encryptionHelp": "Backups für zusätzliche Sicherheit verschlüsseln",
"encryptionPassphrase": "Verschlüsselungs-Passphrase",
"encryptionPassphraseHelp": "Starke Passphrase für Backup-Verschlüsselung",
"confirmPassphrase": "Passphrase bestätigen",
"passphrasesDontMatch": "Passphrasen stimmen nicht überein"
},
"validation": {
"requiredFields": "Bitte füllen Sie alle erforderlichen Felder aus",
"invalidCron": "Ungültiger Cron-Ausdruck",
"connectionTestFailed": "Verbindungstest fehlgeschlagen",
"connectionTestSuccess": "Verbindungstest erfolgreich!"
},
"messages": {
"requiredFields": "Bitte füllen Sie alle erforderlichen Felder aus",
"connectionSuccess": "Verbindungstest erfolgreich!",
"connectionFailed": "Verbindungstest fehlgeschlagen"
},
"testingConnection": "Teste Verbindung...",
"saveSettings": "Konfiguration speichern",
"savingSettings": "Speichern..."
},
"history": {
"searchPlaceholder": "Backups suchen...",
"allStatus": "Alle Status",
"status": {
"completed": "Abgeschlossen",
"failed": "Fehlgeschlagen",
"running": "Läuft",
"partial": "Teilweise"
},
"deleteConfirm": "Sind Sie sicher, dass Sie dieses Backup vom {{date}} löschen möchten?",
"noBackups": "Keine Backups gefunden",
"tableHeaders": {
"date": "Datum",
"type": "Typ",
"status": "Status",
"size": "Größe",
"duration": "Dauer",
"actions": "Aktionen"
},
"columns": {
"status": "Status",
"dateTime": "Datum & Zeit",
"type": "Typ",
"size": "Größe",
"duration": "Dauer",
"actions": "Aktionen"
},
"details": "Details",
"statistics": "Statistiken",
"errors": "Fehler",
"backupDetails": {
"backupId": "Backup-ID",
"startTime": "Startzeit",
"endTime": "Endzeit",
"destination": "Ziel",
"filesProcessed": "Verarbeitete Dateien",
"totalSize": "Gesamtgröße",
"compressionRatio": "Komprimierungsverhältnis",
"errorLog": "Fehlerprotokoll",
"noErrors": "Keine Fehler aufgetreten"
},
"pagination": {
"showing": "Zeige {{from}}-{{to}} von {{total}} Backups",
"previous": "Zurück",
"next": "Weiter"
},
"filter": {
"allStatus": "Alle Status",
"completed": "Abgeschlossen",
"failed": "Fehlgeschlagen",
"running": "Läuft",
"partial": "Teilweise"
},
"noBackupsFound": "Keine Backups gefunden",
"backupsWillAppear": "Backups werden hier angezeigt, sobald sie erstellt wurden",
"messages": {
"deleteSuccess": "Backup erfolgreich gelöscht"
},
"details": {
"backupDetails": "Backup-Details",
"destination": "Ziel",
"started": "Gestartet",
"completed": "Abgeschlossen",
"contentBackedUp": "Gesicherter Inhalt",
"errorDetails": "Fehlerdetails",
"manifest": "Manifest"
}
},
"restore": {
"steps": {
"selectSource": "Quelle auswählen",
"chooseBackup": "Backup auswählen",
"restoreOptions": "Wiederherstellungsoptionen",
"reviewConfirm": "Überprüfen & Bestätigen",
"progress": "Wiederherstellungsfortschritt"
},
"source": {
"title": "Backup-Quelle auswählen",
"subtitle": "Wählen Sie, woher das Backup wiederhergestellt werden soll",
"local": {
"name": "Lokales Backup",
"description": "Vom lokalen Dateisystem wiederherstellen"
},
"s3": {
"name": "S3-Speicher",
"description": "Aus S3-Bucket wiederherstellen"
},
"upload": {
"name": "Backup hochladen",
"description": "Eine Backup-Datei hochladen",
"comingSoon": "Upload-Funktion kommt bald"
},
"configuration": {
"s3": "S3-Konfiguration",
"endpoint": "S3-Endpunkt-URL",
"bucket": "Bucket-Name",
"accessKey": "Zugriffsschlüssel-ID",
"secretKey": "Geheimer Zugriffsschlüssel"
}
},
"backup": {
"title": "Backup für Wiederherstellung auswählen",
"subtitle": "Aus verfügbaren Backups auswählen",
"noBackupsFound": "Keine Backups in ausgewählter Quelle gefunden",
"encrypted": "Verschlüsseltes Backup",
"encryptedMessage": "Sie müssen die Verschlüsselungs-Passphrase angeben, um dieses Backup wiederherzustellen.",
"enterPassphrase": "Verschlüsselungs-Passphrase eingeben",
"at": "um"
},
"restoreTypes": {
"full": {
"name": "Vollständige Wiederherstellung",
"description": "Alles wiederherstellen, einschließlich Datenbank, Fotos und Archive",
"warning": "Dies ersetzt alle aktuellen Daten"
},
"database": {
"name": "Nur Datenbank",
"description": "Nur die Datenbank wiederherstellen (Einstellungen, Veranstaltungen, Benutzer)",
"warning": "Aktuelle Datenbank wird ersetzt"
},
"files": {
"name": "Nur Dateien",
"description": "Nur Fotos und Archive wiederherstellen",
"warning": "Vorhandene Dateien können überschrieben werden"
},
"selective": {
"name": "Selektive Wiederherstellung",
"description": "Bestimmte Elemente zur Wiederherstellung auswählen",
"warning": "Nur ausgewählte Elemente werden wiederhergestellt"
}
},
"options": {
"title": "Wiederherstellungsoptionen",
"subtitle": "Wählen Sie, was wiederhergestellt werden soll",
"additionalOptions": {
"title": "Zusätzliche Optionen",
"skipPreBackup": "Vor-Wiederherstellungs-Backup überspringen",
"skipPreBackupHelp": "Standardmäßig wird vor der Wiederherstellung ein Backup erstellt. Aktivieren Sie dies, um es zu überspringen.",
"force": "Wiederherstellung erzwingen",
"forceHelp": "Sicherheitsprüfungen und Warnungen überschreiben (mit Vorsicht verwenden)"
}
},
"confirmation": {
"title": "Überprüfen & Bestätigen",
"subtitle": "Bitte überprüfen Sie Ihre Wiederherstellungskonfiguration",
"validation": {
"passed": "Validierung bestanden",
"failed": "Validierung fehlgeschlagen",
"checking": "Validiere Wiederherstellungskonfiguration..."
},
"spaceCheck": {
"title": "Speicherplatz",
"required": "Erforderlich",
"available": "Verfügbar",
"insufficient": "Unzureichender Speicherplatz"
},
"summary": {
"title": "Wiederherstellungszusammenfassung",
"source": "Quelle",
"backupDate": "Backup-Datum",
"restoreType": "Wiederherstellungstyp",
"preBackup": "Vor-Backup",
"enabled": "Aktiviert",
"skipped": "Übersprungen"
},
"warning": {
"title": "Wichtiger Hinweis",
"message": "Diese Wiederherstellungsoperation ersetzt vorhandene Daten. Stellen Sie sicher, dass Sie ein aktuelles Backup haben, bevor Sie fortfahren. Diese Aktion kann nicht rückgängig gemacht werden."
}
},
"progress": {
"title": "Wiederherstellungsfortschritt",
"inProgress": "Wiederherstellung läuft...",
"completed": "Wiederherstellung abgeschlossen",
"overallProgress": "Gesamtfortschritt",
"current": "Aktuell",
"statusDetails": "Status-Details",
"restoreLogs": "Wiederherstellungsprotokolle",
"steps": {
"completed": "Abgeschlossen",
"running": "Läuft",
"failed": "Fehlgeschlagen",
"pending": "Ausstehend"
},
"success": {
"title": "Wiederherstellung erfolgreich abgeschlossen",
"message": "Ihre Daten wurden wiederhergestellt. Bitte überprüfen Sie, ob alles korrekt funktioniert."
}
},
"actions": {
"back": "Zurück",
"next": "Weiter",
"startRestore": "Wiederherstellung starten",
"starting": "Starte...",
"validating": "Validiere...",
"startNewRestore": "Neue Wiederherstellung starten"
},
"messages": {
"restoreStarted": "Wiederherstellung erfolgreich gestartet"
}
},
"messages": {
"backupStarted": "Backup erfolgreich gestartet",
"backupFailed": "Backup konnte nicht gestartet werden",
"configUpdated": "Backup-Konfiguration aktualisiert",
"configUpdateFailed": "Konfiguration konnte nicht aktualisiert werden",
"backupDeleted": "Backup erfolgreich gelöscht",
"deleteFailed": "Backup konnte nicht gelöscht werden",
"testEmailSent": "Verbindungstest erfolgreich!",
"testEmailFailed": "Verbindungstest fehlgeschlagen"
}
},
"maintenance": {
"title": "Systemwartung",
"message": "Wir führen derzeit geplante Wartungsarbeiten durch, um unseren Service zu verbessern. Wir sind in Kürze wieder online.",
+404
View File
@@ -953,6 +953,410 @@
"datenschutz": "Privacy Policy",
"pageUpdated": "Page updated successfully"
},
"backup": {
"title": "Backup Management",
"subtitle": "Manage system backups, configure automated backups, and restore from previous backups.",
"tabs": {
"dashboard": "Dashboard",
"configuration": "Configuration",
"history": "Backup History",
"restore": "Restore"
},
"status": {
"inProgress": "Backup in progress...",
"lastBackup": "Last backup",
"noBackups": "No backups found",
"nextBackup": "Next backup",
"notScheduled": "Not scheduled",
"enabled": "Enabled",
"disabled": "Disabled"
},
"actions": {
"runBackupNow": "Run Backup Now",
"starting": "Starting...",
"running": "Running...",
"testConnection": "Test Connection",
"save": "Save Configuration",
"delete": "Delete",
"view": "View Details",
"download": "Download",
"refresh": "Refresh"
},
"dashboard": {
"backupHealth": "Backup Health",
"healthStatus": {
"excellent": "Excellent",
"good": "Good",
"warning": "Warning",
"critical": "Critical"
},
"health": {
"title": "Backup Health"
},
"healthMessages": {
"noBackups": "No backups found",
"lastBackupFailed": "Last backup failed",
"upToDate": "Backup is up to date",
"recent": "Backup is recent",
"gettingOld": "Backup is getting old",
"outdated": "Backup is outdated"
},
"stats": {
"totalBackups": "Total Backups",
"backupSize": "Backup Size",
"lastDuration": "Last Duration",
"backupStatus": "Backup Status",
"last": "Last",
"files": "files",
"minutes": "{{count}}m",
"active": "Active",
"inactive": "Inactive",
"noBackupsYet": "No backups yet"
},
"recentActivity": {
"title": "Recent Backup Activity"
},
"notConfigured": {
"title": "Backup Not Configured",
"message": "Please configure backup settings in the Configuration tab before running backups."
},
"coverage": {
"title": "Backup Coverage",
"database": "Database",
"photos": "Photos",
"archives": "Archives",
"systemFiles": "System Files",
"included": "Included",
"excluded": "Excluded",
"optional": "Optional"
},
"storageDestination": "Storage Destination",
"nextScheduledBackup": "Next Scheduled Backup",
"backupType": "{{type}} backup",
"noDestinationSet": "No destination set"
},
"configuration": {
"enableBackup": "Enable Automated Backups",
"enableBackupHelp": "Automatically create backups according to the configured schedule",
"destinationType": "Backup Destination",
"destinationTypes": {
"local": {
"name": "Local Storage",
"description": "Store backups on the local server filesystem"
},
"rsync": {
"name": "Remote Server (Rsync)",
"description": "Sync backups to a remote server via SSH/Rsync"
},
"s3": {
"name": "S3 Compatible Storage",
"description": "Store backups in Amazon S3 or compatible object storage"
}
},
"fields": {
"destinationPath": "Destination Path",
"destinationPathHelp": "Local directory path for storing backups",
"destinationPathPlaceholder": "/path/to/backup/directory",
"rsyncHost": "Remote Host",
"rsyncHostHelp": "SSH hostname or IP address",
"rsyncHostPlaceholder": "backup.example.com",
"rsyncUser": "SSH User",
"rsyncUserHelp": "Username for SSH connection",
"rsyncUserPlaceholder": "backup-user",
"rsyncPath": "Remote Path",
"rsyncPathHelp": "Directory path on remote server",
"rsyncPathPlaceholder": "/home/backup/photo-sharing",
"rsyncSshKey": "SSH Private Key",
"rsyncSshKeyHelp": "SSH private key for authentication (optional)",
"rsyncSshKeyPlaceholder": "-----BEGIN RSA PRIVATE KEY-----",
"s3Endpoint": "S3 Endpoint",
"s3EndpointHelp": "S3 API endpoint (e.g., s3.amazonaws.com)",
"s3EndpointPlaceholder": "https://s3.amazonaws.com",
"s3Bucket": "Bucket Name",
"s3BucketHelp": "S3 bucket for storing backups",
"s3BucketPlaceholder": "my-backup-bucket",
"s3AccessKey": "Access Key ID",
"s3AccessKeyHelp": "AWS/S3 access key ID",
"s3AccessKeyPlaceholder": "AKIAIOSFODNN7EXAMPLE",
"s3SecretKey": "Secret Access Key",
"s3SecretKeyHelp": "AWS/S3 secret access key",
"s3SecretKeyPlaceholder": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"s3Region": "Region",
"s3RegionHelp": "S3 region (e.g., us-east-1)",
"s3RegionPlaceholder": "us-east-1"
},
"schedule": {
"title": "Backup Schedule",
"scheduleType": "Schedule Type",
"scheduleOptions": {
"hourly": "Every hour",
"daily": "Daily",
"weekly": "Weekly",
"custom": "Custom cron expression"
},
"options": {
"hourly": "Every hour",
"daily": "Daily",
"weekly": "Weekly",
"custom": "Custom cron expression"
},
"customCron": "Cron Expression",
"customCronHelp": "Enter a valid cron expression (e.g., 0 3 * * *)",
"retention": "Retention Period",
"retentionDays": "Keep backups for",
"retentionHelp": "days (older backups will be automatically deleted)"
},
"whatToBackup": {
"title": "What to Backup",
"database": "Database",
"databaseHelp": "All event data, settings, and configurations",
"photos": "Photos",
"photosHelp": "All uploaded photos in active galleries",
"archives": "Archives",
"archivesHelp": "Archived event ZIP files",
"thumbnails": "Thumbnails",
"thumbnailsHelp": "Generated thumbnail images (can be recreated)",
"tempFiles": "Temporary Files",
"tempFilesHelp": "Temporary upload and processing files"
},
"advancedOptions": {
"title": "Advanced Options",
"compression": "Enable Compression",
"compressionHelp": "Compress backup files to save storage space",
"encryption": "Enable Encryption",
"encryptionHelp": "Encrypt backups for additional security",
"encryptionPassphrase": "Encryption Passphrase",
"encryptionPassphraseHelp": "Strong passphrase for backup encryption",
"confirmPassphrase": "Confirm Passphrase",
"passphrasesDontMatch": "Passphrases don't match"
},
"validation": {
"requiredFields": "Please fill in all required fields",
"invalidCron": "Invalid cron expression",
"connectionTestFailed": "Connection test failed",
"connectionTestSuccess": "Connection test successful!"
},
"messages": {
"requiredFields": "Please fill in all required fields",
"connectionSuccess": "Connection test successful!",
"connectionFailed": "Connection test failed"
},
"testingConnection": "Testing connection...",
"saveSettings": "Save Configuration",
"savingSettings": "Saving..."
},
"history": {
"searchPlaceholder": "Search backups...",
"allStatus": "All Status",
"status": {
"completed": "Completed",
"failed": "Failed",
"running": "Running",
"partial": "Partial"
},
"deleteConfirm": "Are you sure you want to delete this backup from {{date}}?",
"noBackups": "No backups found",
"tableHeaders": {
"date": "Date",
"type": "Type",
"status": "Status",
"size": "Size",
"duration": "Duration",
"actions": "Actions"
},
"columns": {
"status": "Status",
"dateTime": "Date & Time",
"type": "Type",
"size": "Size",
"duration": "Duration",
"actions": "Actions"
},
"details": "Details",
"statistics": "Statistics",
"errors": "Errors",
"backupDetails": {
"backupId": "Backup ID",
"startTime": "Start Time",
"endTime": "End Time",
"destination": "Destination",
"filesProcessed": "Files Processed",
"totalSize": "Total Size",
"compressionRatio": "Compression Ratio",
"errorLog": "Error Log",
"noErrors": "No errors occurred"
},
"pagination": {
"showing": "Showing {{from}}-{{to}} of {{total}} backups",
"previous": "Previous",
"next": "Next"
},
"filter": {
"allStatus": "All Status",
"completed": "Completed",
"failed": "Failed",
"running": "Running",
"partial": "Partial"
},
"noBackupsFound": "No backups found",
"backupsWillAppear": "Backups will appear here once created",
"messages": {
"deleteSuccess": "Backup deleted successfully"
},
"details": {
"backupDetails": "Backup Details",
"destination": "Destination",
"started": "Started",
"completed": "Completed",
"contentBackedUp": "Content Backed Up",
"errorDetails": "Error Details",
"manifest": "Manifest"
}
},
"restore": {
"steps": {
"selectSource": "Select Source",
"chooseBackup": "Choose Backup",
"restoreOptions": "Restore Options",
"reviewConfirm": "Review & Confirm",
"progress": "Restore Progress"
},
"source": {
"title": "Select Backup Source",
"subtitle": "Choose where to restore the backup from",
"local": {
"name": "Local Backup",
"description": "Restore from local filesystem"
},
"s3": {
"name": "S3 Storage",
"description": "Restore from S3 bucket"
},
"upload": {
"name": "Upload Backup",
"description": "Upload a backup file",
"comingSoon": "Upload functionality coming soon"
},
"configuration": {
"s3": "S3 Configuration",
"endpoint": "S3 Endpoint URL",
"bucket": "Bucket Name",
"accessKey": "Access Key ID",
"secretKey": "Secret Access Key"
}
},
"backup": {
"title": "Choose Backup to Restore",
"subtitle": "Select from available backups",
"noBackupsFound": "No backups found in selected source",
"encrypted": "Encrypted Backup",
"encryptedMessage": "You'll need to provide the encryption passphrase to restore this backup.",
"enterPassphrase": "Enter encryption passphrase",
"at": "at"
},
"restoreTypes": {
"full": {
"name": "Full Restore",
"description": "Restore everything including database, photos, and archives",
"warning": "This will replace all current data"
},
"database": {
"name": "Database Only",
"description": "Restore only the database (settings, events, users)",
"warning": "Current database will be replaced"
},
"files": {
"name": "Files Only",
"description": "Restore only photos and archives",
"warning": "Existing files may be overwritten"
},
"selective": {
"name": "Selective Restore",
"description": "Choose specific items to restore",
"warning": "Only selected items will be restored"
}
},
"options": {
"title": "Restore Options",
"subtitle": "Choose what to restore",
"additionalOptions": {
"title": "Additional Options",
"skipPreBackup": "Skip Pre-Restore Backup",
"skipPreBackupHelp": "By default, a backup is created before restore. Check this to skip it.",
"force": "Force Restore",
"forceHelp": "Override safety checks and warnings (use with caution)"
}
},
"confirmation": {
"title": "Review & Confirm",
"subtitle": "Please review your restore configuration",
"validation": {
"passed": "Validation Passed",
"failed": "Validation Failed",
"checking": "Validating restore configuration..."
},
"spaceCheck": {
"title": "Storage Space",
"required": "Required",
"available": "Available",
"insufficient": "Insufficient storage space"
},
"summary": {
"title": "Restore Summary",
"source": "Source",
"backupDate": "Backup Date",
"restoreType": "Restore Type",
"preBackup": "Pre-backup",
"enabled": "Enabled",
"skipped": "Skipped"
},
"warning": {
"title": "Important Notice",
"message": "This restore operation will replace existing data. Make sure you have a current backup before proceeding. This action cannot be undone."
}
},
"progress": {
"title": "Restore Progress",
"inProgress": "Restore in progress...",
"completed": "Restore completed",
"overallProgress": "Overall Progress",
"current": "Current",
"statusDetails": "Status Details",
"restoreLogs": "Restore Logs",
"steps": {
"completed": "Completed",
"running": "Running",
"failed": "Failed",
"pending": "Pending"
},
"success": {
"title": "Restore Completed Successfully",
"message": "Your data has been restored. Please verify everything is working correctly."
}
},
"actions": {
"back": "Back",
"next": "Next",
"startRestore": "Start Restore",
"starting": "Starting...",
"validating": "Validating...",
"startNewRestore": "Start New Restore"
},
"messages": {
"restoreStarted": "Restore started successfully"
}
},
"messages": {
"backupStarted": "Backup started successfully",
"backupFailed": "Failed to start backup",
"configUpdated": "Backup configuration updated",
"configUpdateFailed": "Failed to update configuration",
"backupDeleted": "Backup deleted successfully",
"deleteFailed": "Failed to delete backup",
"testEmailSent": "Test connection successful!",
"testEmailFailed": "Connection test failed"
}
},
"maintenance": {
"title": "System Maintenance",
"message": "We're currently performing scheduled maintenance to improve our service. We'll be back online shortly.",
+24 -20
View File
@@ -25,6 +25,7 @@ import {
import { toast } from 'react-toastify';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { format } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { Button, Card, Loading } from '../../components/common';
import { BackupDashboard } from '../../components/admin/BackupDashboard';
@@ -33,17 +34,20 @@ import { BackupHistory } from '../../components/admin/BackupHistory';
import { RestoreWizard } from '../../components/admin/RestoreWizard';
import { api } from '../../config/api';
// Tab components
const tabs = [
{ id: 'dashboard', label: 'Dashboard', icon: HardDrive },
{ id: 'configuration', label: 'Configuration', icon: Settings },
{ id: 'history', label: 'Backup History', icon: History },
{ id: 'restore', label: 'Restore', icon: RefreshCw }
];
// Tab components will be defined inside the component to use translations
export const BackupManagement = () => {
const [activeTab, setActiveTab] = useState('dashboard');
const queryClient = useQueryClient();
const { t } = useTranslation();
// Tab components with translations
const tabs = [
{ id: 'dashboard', label: t('backup.tabs.dashboard'), icon: HardDrive },
{ id: 'configuration', label: t('backup.tabs.configuration'), icon: Settings },
{ id: 'history', label: t('backup.tabs.history'), icon: History },
{ id: 'restore', label: t('backup.tabs.restore'), icon: RefreshCw }
];
// Fetch backup status
const { data: backupStatus, isLoading: statusLoading } = useQuery({
@@ -71,11 +75,11 @@ export const BackupManagement = () => {
return response.data;
},
onSuccess: () => {
toast.success('Backup started successfully');
toast.success(t('backup.messages.backupStarted'));
queryClient.invalidateQueries({ queryKey: ['backup-status'] });
},
onError: (error) => {
const message = error.response?.data?.error || 'Failed to start backup';
const message = error.response?.data?.error || t('backup.messages.backupFailed');
toast.error(message);
}
});
@@ -87,11 +91,11 @@ export const BackupManagement = () => {
return response.data;
},
onSuccess: () => {
toast.success('Backup configuration updated');
toast.success(t('backup.messages.configUpdated'));
queryClient.invalidateQueries({ queryKey: ['backup-config'] });
},
onError: (error) => {
const message = error.response?.data?.error || 'Failed to update configuration';
const message = error.response?.data?.error || t('backup.messages.configUpdateFailed');
toast.error(message);
}
});
@@ -108,9 +112,9 @@ export const BackupManagement = () => {
<div className="p-8 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Backup Management</h1>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('backup.title')}</h1>
<p className="text-gray-600">
Manage system backups, configure automated backups, and restore from previous backups.
{t('backup.subtitle')}
</p>
</div>
@@ -122,19 +126,19 @@ export const BackupManagement = () => {
{backupStatus?.isRunning ? (
<>
<Loader2 className="h-5 w-5 text-blue-500 animate-spin" />
<span className="text-blue-600 font-medium">Backup in progress...</span>
<span className="text-blue-600 font-medium">{t('backup.status.inProgress')}</span>
</>
) : backupStatus?.lastBackup ? (
<>
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-gray-700">
Last backup: {format(new Date(backupStatus.lastBackup.created_at), 'PPp')}
{t('backup.status.lastBackup')}: {format(new Date(backupStatus.lastBackup.created_at), 'PPp')}
</span>
</>
) : (
<>
<AlertCircle className="h-5 w-5 text-amber-500" />
<span className="text-gray-700">No backups found</span>
<span className="text-gray-700">{t('backup.status.noBackups')}</span>
</>
)}
</div>
@@ -143,7 +147,7 @@ export const BackupManagement = () => {
<div className="flex items-center space-x-2">
<Clock className="h-5 w-5 text-gray-400" />
<span className="text-sm text-gray-600">
Next backup: {backupStatus?.nextBackup || 'Not scheduled'}
{t('backup.status.nextBackup')}: {backupStatus?.nextBackup || t('backup.status.notScheduled')}
</span>
</div>
)}
@@ -159,12 +163,12 @@ export const BackupManagement = () => {
{manualBackupMutation.isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Starting...
{t('backup.actions.starting')}
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Run Backup Now
{t('backup.actions.runBackupNow')}
</>
)}
</Button>
@@ -175,7 +179,7 @@ export const BackupManagement = () => {
: 'bg-gray-100 text-gray-700'
}`}>
<Shield className="h-4 w-4" />
<span>{backupConfig?.backup_enabled ? 'Enabled' : 'Disabled'}</span>
<span>{backupConfig?.backup_enabled ? t('backup.status.enabled') : t('backup.status.disabled')}</span>
</div>
</div>
</div>