various fixes

This commit is contained in:
Stefan Hardegger
2025-08-11 08:15:20 +02:00
parent 5d195b63ef
commit 51e3d20c24
6 changed files with 520 additions and 71 deletions

View File

@@ -40,13 +40,13 @@ export default function SettingsPage() {
const [authorsSchema, setAuthorsSchema] = useState<any>(null);
const [showSchema, setShowSchema] = useState(false);
const [databaseStatus, setDatabaseStatus] = useState<{
backup: { loading: boolean; message: string; success?: boolean };
restore: { loading: boolean; message: string; success?: boolean };
clear: { loading: boolean; message: string; success?: boolean };
completeBackup: { loading: boolean; message: string; success?: boolean };
completeRestore: { loading: boolean; message: string; success?: boolean };
completeClear: { loading: boolean; message: string; success?: boolean };
}>({
backup: { loading: false, message: '' },
restore: { loading: false, message: '' },
clear: { loading: false, message: '' }
completeBackup: { loading: false, message: '' },
completeRestore: { loading: false, message: '' },
completeClear: { loading: false, message: '' }
});
// Load settings from localStorage on mount
@@ -166,14 +166,15 @@ export default function SettingsPage() {
}
};
const handleDatabaseBackup = async () => {
const handleCompleteBackup = async () => {
setDatabaseStatus(prev => ({
...prev,
backup: { loading: true, message: 'Creating backup...', success: undefined }
completeBackup: { loading: true, message: 'Creating complete backup...', success: undefined }
}));
try {
const backupBlob = await databaseApi.backup();
const backupBlob = await databaseApi.backupComplete();
// Create download link
const url = window.URL.createObjectURL(backupBlob);
@@ -181,7 +182,7 @@ export default function SettingsPage() {
link.href = url;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
link.download = `storycove_backup_${timestamp}.sql`;
link.download = `storycove_complete_backup_${timestamp}.zip`;
document.body.appendChild(link);
link.click();
@@ -190,12 +191,12 @@ export default function SettingsPage() {
setDatabaseStatus(prev => ({
...prev,
backup: { loading: false, message: 'Backup downloaded successfully', success: true }
completeBackup: { loading: false, message: 'Complete backup downloaded successfully', success: true }
}));
} catch (error: any) {
setDatabaseStatus(prev => ({
...prev,
backup: { loading: false, message: error.message || 'Backup failed', success: false }
completeBackup: { loading: false, message: error.message || 'Complete backup failed', success: false }
}));
}
@@ -203,42 +204,42 @@ export default function SettingsPage() {
setTimeout(() => {
setDatabaseStatus(prev => ({
...prev,
backup: { loading: false, message: '', success: undefined }
completeBackup: { loading: false, message: '', success: undefined }
}));
}, 5000);
};
const handleDatabaseRestore = async (event: React.ChangeEvent<HTMLInputElement>) => {
const handleCompleteRestore = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Reset the input so the same file can be selected again
event.target.value = '';
if (!file.name.endsWith('.sql')) {
if (!file.name.endsWith('.zip')) {
setDatabaseStatus(prev => ({
...prev,
restore: { loading: false, message: 'Please select a .sql file', success: false }
completeRestore: { loading: false, message: 'Please select a .zip file', success: false }
}));
return;
}
const confirmed = window.confirm(
'Are you sure you want to restore the database? This will PERMANENTLY DELETE all current data and replace it with the backup data. This action cannot be undone!'
'Are you sure you want to restore the complete backup? This will PERMANENTLY DELETE all current data AND files (cover images, avatars) and replace them with the backup data. This action cannot be undone!'
);
if (!confirmed) return;
setDatabaseStatus(prev => ({
...prev,
restore: { loading: true, message: 'Restoring database...', success: undefined }
completeRestore: { loading: true, message: 'Restoring complete backup...', success: undefined }
}));
try {
const result = await databaseApi.restore(file);
const result = await databaseApi.restoreComplete(file);
setDatabaseStatus(prev => ({
...prev,
restore: {
completeRestore: {
loading: false,
message: result.success ? result.message : result.message,
success: result.success
@@ -247,7 +248,7 @@ export default function SettingsPage() {
} catch (error: any) {
setDatabaseStatus(prev => ({
...prev,
restore: { loading: false, message: error.message || 'Restore failed', success: false }
completeRestore: { loading: false, message: error.message || 'Complete restore failed', success: false }
}));
}
@@ -255,37 +256,37 @@ export default function SettingsPage() {
setTimeout(() => {
setDatabaseStatus(prev => ({
...prev,
restore: { loading: false, message: '', success: undefined }
completeRestore: { loading: false, message: '', success: undefined }
}));
}, 10000);
};
const handleDatabaseClear = async () => {
const handleCompleteClear = async () => {
const confirmed = window.confirm(
'Are you ABSOLUTELY SURE you want to clear the entire database? This will PERMANENTLY DELETE ALL stories, authors, series, tags, and collections. This action cannot be undone!'
'Are you ABSOLUTELY SURE you want to clear the entire database AND all files? This will PERMANENTLY DELETE ALL stories, authors, series, tags, collections, AND all uploaded images (covers, avatars). This action cannot be undone!'
);
if (!confirmed) return;
const doubleConfirmed = window.confirm(
'This is your final warning! Clicking OK will DELETE EVERYTHING in your StoryCove database. Are you completely certain you want to proceed?'
'This is your final warning! Clicking OK will DELETE EVERYTHING in your StoryCove database AND all uploaded files. Are you completely certain you want to proceed?'
);
if (!doubleConfirmed) return;
setDatabaseStatus(prev => ({
...prev,
clear: { loading: true, message: 'Clearing database...', success: undefined }
completeClear: { loading: true, message: 'Clearing database and files...', success: undefined }
}));
try {
const result = await databaseApi.clear();
const result = await databaseApi.clearComplete();
setDatabaseStatus(prev => ({
...prev,
clear: {
completeClear: {
loading: false,
message: result.success
? `Database cleared successfully. Deleted ${result.deletedRecords} records.`
? `Database and files cleared successfully. Deleted ${result.deletedRecords} records.`
: result.message,
success: result.success
}
@@ -293,7 +294,7 @@ export default function SettingsPage() {
} catch (error: any) {
setDatabaseStatus(prev => ({
...prev,
clear: { loading: false, message: error.message || 'Clear operation failed', success: false }
completeClear: { loading: false, message: error.message || 'Clear operation failed', success: false }
}));
}
@@ -301,7 +302,7 @@ export default function SettingsPage() {
setTimeout(() => {
setDatabaseStatus(prev => ({
...prev,
clear: { loading: false, message: '', success: undefined }
completeClear: { loading: false, message: '', success: undefined }
}));
}, 10000);
};
@@ -616,90 +617,90 @@ export default function SettingsPage() {
<div className="theme-card theme-shadow rounded-lg p-6">
<h2 className="text-xl font-semibold theme-header mb-4">Database Management</h2>
<p className="theme-text mb-6">
Backup, restore, or clear your StoryCove database. These are powerful tools - use with caution!
Backup, restore, or clear your StoryCove database and files. These comprehensive operations include both your data and uploaded images.
</p>
<div className="space-y-6">
{/* Backup Section */}
<div className="border theme-border rounded-lg p-4">
<h3 className="text-lg font-semibold theme-header mb-3">💾 Backup Database</h3>
{/* Complete Backup Section */}
<div className="border theme-border rounded-lg p-4 border-blue-200 dark:border-blue-800">
<h3 className="text-lg font-semibold theme-header mb-3">📦 Create Backup</h3>
<p className="text-sm theme-text mb-3">
Download a complete backup of your database as an SQL file. This includes all stories, authors, tags, series, and collections.
Download a complete backup as a ZIP file. This includes your database AND all uploaded files (cover images, avatars). This is a comprehensive backup of your entire StoryCove installation.
</p>
<Button
onClick={handleDatabaseBackup}
disabled={databaseStatus.backup.loading}
loading={databaseStatus.backup.loading}
onClick={handleCompleteBackup}
disabled={databaseStatus.completeBackup.loading}
loading={databaseStatus.completeBackup.loading}
variant="primary"
className="w-full sm:w-auto"
>
{databaseStatus.backup.loading ? 'Creating Backup...' : 'Download Backup'}
{databaseStatus.completeBackup.loading ? 'Creating Backup...' : 'Download Backup'}
</Button>
{databaseStatus.backup.message && (
{databaseStatus.completeBackup.message && (
<div className={`text-sm p-2 rounded mt-3 ${
databaseStatus.backup.success
databaseStatus.completeBackup.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{databaseStatus.backup.message}
{databaseStatus.completeBackup.message}
</div>
)}
</div>
{/* Restore Section */}
<div className="border theme-border rounded-lg p-4 border-orange-200 dark:border-orange-800">
<h3 className="text-lg font-semibold theme-header mb-3">📥 Restore Database</h3>
<h3 className="text-lg font-semibold theme-header mb-3">📥 Restore Backup</h3>
<p className="text-sm theme-text mb-3">
<strong className="text-orange-600 dark:text-orange-400">⚠️ Warning:</strong> This will completely replace your current database with the backup file. All existing data will be permanently deleted.
<strong className="text-orange-600 dark:text-orange-400">⚠️ Warning:</strong> This will completely replace your current database AND all files with the backup. All existing data and uploaded files will be permanently deleted.
</p>
<div className="flex items-center gap-3">
<input
type="file"
accept=".sql"
onChange={handleDatabaseRestore}
disabled={databaseStatus.restore.loading}
accept=".zip"
onChange={handleCompleteRestore}
disabled={databaseStatus.completeRestore.loading}
className="flex-1 text-sm theme-text file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:theme-accent-bg file:text-white hover:file:bg-opacity-90 file:cursor-pointer"
/>
</div>
{databaseStatus.restore.message && (
{databaseStatus.completeRestore.message && (
<div className={`text-sm p-2 rounded mt-3 ${
databaseStatus.restore.success
databaseStatus.completeRestore.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{databaseStatus.restore.message}
{databaseStatus.completeRestore.message}
</div>
)}
{databaseStatus.restore.loading && (
{databaseStatus.completeRestore.loading && (
<div className="text-sm theme-text mt-3 flex items-center gap-2">
<div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
Restoring database...
Restoring backup...
</div>
)}
</div>
{/* Clear Section */}
<div className="border theme-border rounded-lg p-4 border-red-200 dark:border-red-800">
<h3 className="text-lg font-semibold theme-header mb-3">🗑️ Clear Database</h3>
{/* Clear Everything Section */}
<div className="border theme-border rounded-lg p-4 border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/10">
<h3 className="text-lg font-semibold theme-header mb-3">🗑️ Clear Everything</h3>
<p className="text-sm theme-text mb-3">
<strong className="text-red-600 dark:text-red-400">⚠️ Danger Zone:</strong> This will permanently delete ALL data from your database. Stories, authors, tags, series, and collections will be completely removed. This action cannot be undone!
<strong className="text-red-600 dark:text-red-400">⚠️ Danger Zone:</strong> This will permanently delete ALL data from your database AND all uploaded files (cover images, avatars). Everything will be completely removed. This action cannot be undone!
</p>
<Button
onClick={handleDatabaseClear}
disabled={databaseStatus.clear.loading}
loading={databaseStatus.clear.loading}
onClick={handleCompleteClear}
disabled={databaseStatus.completeClear.loading}
loading={databaseStatus.completeClear.loading}
variant="secondary"
className="w-full sm:w-auto bg-red-600 hover:bg-red-700 text-white border-red-600"
className="w-full sm:w-auto bg-red-700 hover:bg-red-800 text-white border-red-700"
>
{databaseStatus.clear.loading ? 'Clearing Database...' : 'Clear All Data'}
{databaseStatus.completeClear.loading ? 'Clearing Everything...' : 'Clear Everything'}
</Button>
{databaseStatus.clear.message && (
{databaseStatus.completeClear.message && (
<div className={`text-sm p-2 rounded mt-3 ${
databaseStatus.clear.success
databaseStatus.completeClear.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{databaseStatus.clear.message}
{databaseStatus.completeClear.message}
</div>
)}
</div>
@@ -708,8 +709,9 @@ export default function SettingsPage() {
<p className="font-medium mb-1">💡 Best Practices:</p>
<ul className="text-xs space-y-1 ml-4">
<li>• <strong>Always backup</strong> before performing restore or clear operations</li>
<li>• <strong>Test restores</strong> in a development environment when possible</li>
<li>• <strong>Store backups safely</strong> in multiple locations for important data</li>
<li>• <strong>Test restores</strong> in a development environment when possible</li>
<li>• <strong>Backup files (.zip)</strong> contain both database and all uploaded files</li>
<li>• <strong>Verify backup files</strong> are complete before relying on them</li>
</ul>
</div>

View File

@@ -520,6 +520,27 @@ export const databaseApi = {
const response = await api.post('/database/clear');
return response.data;
},
backupComplete: async (): Promise<Blob> => {
const response = await api.post('/database/backup-complete', {}, {
responseType: 'blob'
});
return response.data;
},
restoreComplete: async (file: File): Promise<{ success: boolean; message: string }> => {
const formData = new FormData();
formData.append('file', file);
const response = await api.post('/database/restore-complete', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
},
clearComplete: async (): Promise<{ success: boolean; message: string; deletedRecords?: number }> => {
const response = await api.post('/database/clear-complete');
return response.data;
},
};
// Image utility

File diff suppressed because one or more lines are too long