diff --git a/py/api.py b/py/api.py
index 2c7b06c..fb2bc1b 100644
--- a/py/api.py
+++ b/py/api.py
@@ -200,6 +200,15 @@ class PromptManagerAPI:
async def export_prompts_route(request):
return await self.export_prompts(request)
+ # Database backup and restore
+ @routes.get("/prompt_manager/backup")
+ async def backup_database_route(request):
+ return await self.backup_database(request)
+
+ @routes.post("/prompt_manager/restore")
+ async def restore_database_route(request):
+ return await self.restore_database(request)
+
# Gallery endpoints
@routes.get("/prompt_manager/prompts/{prompt_id}/images")
async def get_prompt_images_route(request):
@@ -1447,3 +1456,164 @@ class PromptManagerAPI:
'success': False,
'error': f'Maintenance failed: {str(e)}'
}, status=500)
+
+ async def backup_database(self, request):
+ """
+ Backup the entire prompts.db database file.
+ GET /prompt_manager/backup
+ """
+ try:
+ import os
+ import shutil
+ import tempfile
+ from pathlib import Path
+
+ # Get the database path
+ db_path = "prompts.db"
+
+ if not os.path.exists(db_path):
+ return web.json_response({
+ 'success': False,
+ 'error': 'Database file not found'
+ }, status=404)
+
+ # Create a temporary copy of the database
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as temp_file:
+ temp_path = temp_file.name
+
+ # Copy the database file
+ shutil.copy2(db_path, temp_path)
+
+ # Read the file content
+ with open(temp_path, 'rb') as f:
+ file_data = f.read()
+
+ # Clean up temporary file
+ os.unlink(temp_path)
+
+ # Generate filename with timestamp
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f"prompts_backup_{timestamp}.db"
+
+ return web.Response(
+ body=file_data,
+ content_type='application/octet-stream',
+ headers={
+ 'Content-Disposition': f'attachment; filename="{filename}"',
+ 'Content-Length': str(len(file_data))
+ }
+ )
+
+ except Exception as e:
+ print(f"[PromptManager API] Backup error: {e}")
+ import traceback
+ traceback.print_exc()
+ return web.json_response({
+ 'success': False,
+ 'error': f'Failed to backup database: {str(e)}'
+ }, status=500)
+
+ async def restore_database(self, request):
+ """
+ Restore the prompts.db database from uploaded file.
+ POST /prompt_manager/restore
+ """
+ try:
+ import os
+ import shutil
+ import tempfile
+ import sqlite3
+ from pathlib import Path
+
+ # Get the uploaded file
+ reader = await request.multipart()
+ field = await reader.next()
+
+ if not field or field.name != 'database_file':
+ return web.json_response({
+ 'success': False,
+ 'error': 'No database file uploaded. Expected field name: database_file'
+ }, status=400)
+
+ # Read the uploaded file content
+ file_data = await field.read()
+
+ if not file_data:
+ return web.json_response({
+ 'success': False,
+ 'error': 'Uploaded file is empty'
+ }, status=400)
+
+ # Create a temporary file to validate the database
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as temp_file:
+ temp_path = temp_file.name
+ temp_file.write(file_data)
+
+ try:
+ # Validate that it's a valid SQLite database with expected structure
+ with sqlite3.connect(temp_path) as conn:
+ conn.row_factory = sqlite3.Row
+
+ # Check if it has the prompts table
+ cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='prompts'")
+ if not cursor.fetchone():
+ raise ValueError("Database does not contain a 'prompts' table")
+
+ # Check basic structure of prompts table
+ cursor = conn.execute("PRAGMA table_info(prompts)")
+ columns = [row['name'] for row in cursor.fetchall()]
+ required_columns = ['id', 'text', 'created_at']
+
+ for col in required_columns:
+ if col not in columns:
+ raise ValueError(f"Database missing required column: {col}")
+
+ # Get basic stats for validation
+ cursor = conn.execute("SELECT COUNT(*) as count FROM prompts")
+ prompt_count = cursor.fetchone()['count']
+
+ # If validation passes, backup current database and restore
+ db_path = "prompts.db"
+ backup_path = f"{db_path}.backup_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
+
+ # Create backup of current database if it exists
+ if os.path.exists(db_path):
+ shutil.copy2(db_path, backup_path)
+ print(f"[PromptManager] Current database backed up to: {backup_path}")
+
+ # Replace current database with uploaded one
+ shutil.copy2(temp_path, db_path)
+
+ # Reinitialize the database connection
+ self.db = PromptDatabase()
+
+ return web.json_response({
+ 'success': True,
+ 'message': f'Database restored successfully. Found {prompt_count} prompts.',
+ 'prompt_count': prompt_count,
+ 'backup_created': backup_path if os.path.exists(db_path) else None
+ })
+
+ except sqlite3.Error as e:
+ return web.json_response({
+ 'success': False,
+ 'error': f'Invalid SQLite database: {str(e)}'
+ }, status=400)
+ except ValueError as e:
+ return web.json_response({
+ 'success': False,
+ 'error': str(e)
+ }, status=400)
+ finally:
+ # Clean up temporary file
+ if os.path.exists(temp_path):
+ os.unlink(temp_path)
+
+ except Exception as e:
+ print(f"[PromptManager API] Restore error: {e}")
+ import traceback
+ traceback.print_exc()
+ return web.json_response({
+ 'success': False,
+ 'error': f'Failed to restore database: {str(e)}'
+ }, status=500)
diff --git a/web/admin.html b/web/admin.html
index 2f9fd1b..a2957b0 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -180,6 +180,14 @@
class="px-4 py-2 bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium rounded-lg transition-colors">
🔧 Maintenance
+
+
@@ -297,6 +305,40 @@
+
+
+
+
📁 Restore Database
+
+
+
+
+
⚠️
+
+
Warning
+
This will replace your current database. A backup will be created automatically.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -535,6 +577,8 @@
document.getElementById("statsBtn").addEventListener("click", () => this.showStats());
document.getElementById("diagnosticsBtn").addEventListener("click", () => this.showDiagnosticsModal());
document.getElementById("maintenanceBtn").addEventListener("click", () => this.showMaintenanceModal());
+ document.getElementById("backupBtn").addEventListener("click", () => this.backupDatabase());
+ document.getElementById("restoreBtn").addEventListener("click", () => this.showRestoreModal());
// Modals
this.bindModalEvents();
@@ -558,6 +602,10 @@
document.getElementById("confirmBulkCategory").addEventListener("click", () => this.confirmBulkCategory());
document.getElementById("cancelBulkCategory").addEventListener("click", () => this.hideModal("bulkCategoryModal"));
+ // Restore modal
+ document.getElementById("confirmRestore").addEventListener("click", () => this.confirmRestore());
+ document.getElementById("cancelRestore").addEventListener("click", () => this.hideModal("restoreModal"));
+
// Diagnostics modal
document.getElementById("runDiagnosticsBtn").addEventListener("click", () => this.runDiagnostics());
document.getElementById("testImageLinkBtn").addEventListener("click", () => this.testImageLink());
@@ -1755,6 +1803,118 @@
}
}
+ // Database backup and restore functionality
+ async backupDatabase() {
+ try {
+ const response = await fetch('/prompt_manager/backup');
+
+ if (response.ok) {
+ // Get the filename from the Content-Disposition header
+ const contentDisposition = response.headers.get('Content-Disposition');
+ let filename = 'prompts_backup.db';
+
+ if (contentDisposition) {
+ const filenameMatch = contentDisposition.match(/filename="(.+)"/);
+ if (filenameMatch) {
+ filename = filenameMatch[1];
+ }
+ }
+
+ // Create blob and download
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ this.showNotification('💾 Database backup downloaded successfully!', 'success');
+ } else {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Backup failed');
+ }
+ } catch (error) {
+ console.error('Backup error:', error);
+ this.showNotification(`❌ Backup failed: ${error.message}`, 'error');
+ }
+ }
+
+ showRestoreModal() {
+ // Clear previous file selection
+ document.getElementById('restoreFileInput').value = '';
+ this.showModal('restoreModal');
+ }
+
+ async confirmRestore() {
+ const fileInput = document.getElementById('restoreFileInput');
+ const file = fileInput.files[0];
+
+ if (!file) {
+ this.showNotification('Please select a database file to restore', 'warning');
+ return;
+ }
+
+ if (!file.name.endsWith('.db')) {
+ this.showNotification('Please select a valid .db file', 'warning');
+ return;
+ }
+
+ // Show confirmation
+ const confirmed = confirm(
+ `Are you sure you want to restore from "${file.name}"?\n\n` +
+ 'This will replace your current database. A backup will be created automatically.\n\n' +
+ 'This action cannot be undone.'
+ );
+
+ if (!confirmed) {
+ return;
+ }
+
+ try {
+ // Disable the restore button and show loading
+ const confirmBtn = document.getElementById('confirmRestore');
+ confirmBtn.disabled = true;
+ confirmBtn.innerHTML = '🔄 Restoring...';
+
+ // Create FormData for file upload
+ const formData = new FormData();
+ formData.append('database_file', file);
+
+ const response = await fetch('/prompt_manager/restore', {
+ method: 'POST',
+ body: formData
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ this.showNotification(
+ `✅ Database restored successfully! Found ${data.prompt_count} prompts.`,
+ 'success'
+ );
+ this.hideModal('restoreModal');
+
+ // Refresh the interface to show new data
+ setTimeout(() => {
+ window.location.reload();
+ }, 2000);
+ } else {
+ throw new Error(data.error || 'Restore failed');
+ }
+ } catch (error) {
+ console.error('Restore error:', error);
+ this.showNotification(`❌ Restore failed: ${error.message}`, 'error');
+ } finally {
+ // Re-enable the restore button
+ const confirmBtn = document.getElementById('confirmRestore');
+ confirmBtn.disabled = false;
+ confirmBtn.innerHTML = 'Restore Database';
+ }
+ }
+
// Copy prompt to clipboard functionality
async copyPromptToClipboard(promptId) {
// Find the prompt text from the prompts array
diff --git a/web/prompt_manager.js b/web/prompt_manager.js
index 0de736d..582490b 100644
--- a/web/prompt_manager.js
+++ b/web/prompt_manager.js
@@ -24,7 +24,7 @@ app.registerExtension({
this.properties.selectedPromptIndex = -1;
this.properties.resultTimeout = 3; // Default 3 seconds
this.properties.showTestButton = false; // Default hide test button
- this.properties.webuiDisplayMode = 'popup'; // Default popup mode
+ this.properties.webuiDisplayMode = "popup"; // Default popup mode
this.resultHideTimer = null;
// Load settings from API
@@ -48,12 +48,12 @@ app.registerExtension({
// Set initial node size, but preserve user resizes
if (!this._userHasResized) {
- this.size = [400, 300]; // width=400, height=300 pixels
+ this.size = [400, 400]; // width=400, height=300 pixels
}
-
+
// Hook into resize to track user changes
const originalOnResize = this.onResize;
- this.onResize = function(size) {
+ this.onResize = function (size) {
this._userHasResized = true;
console.log("[PromptManager] User resized to:", size);
if (originalOnResize) {
@@ -63,7 +63,7 @@ app.registerExtension({
// Hook into serialization to preserve resize flag
const originalSerialize = this.serialize;
- this.serialize = function() {
+ this.serialize = function () {
const data = originalSerialize ? originalSerialize.call(this) : {};
data._userHasResized = this._userHasResized;
return data;
@@ -71,7 +71,7 @@ app.registerExtension({
// Hook into configure to restore resize flag
const originalConfigure = this.configure;
- this.configure = function(data) {
+ this.configure = function (data) {
if (originalConfigure) {
originalConfigure.call(this, data);
}
@@ -259,8 +259,8 @@ app.registerExtension({
this.properties.resultTimeout = data.settings.result_timeout || 3;
this.properties.showTestButton =
data.settings.show_test_button || false;
- this.properties.webuiDisplayMode =
- data.settings.webui_display_mode || 'popup';
+ this.properties.webuiDisplayMode =
+ data.settings.webui_display_mode || "popup";
}
}
} catch (error) {
@@ -625,16 +625,21 @@ app.registerExtension({
const currentOrigin = window.location.origin;
const webUrl = `${currentOrigin}/prompt_manager/web`;
- if (this.properties.webuiDisplayMode === 'newtab') {
+ if (this.properties.webuiDisplayMode === "newtab") {
// Open in new tab without window options
const newWindow = window.open(webUrl, "_blank");
-
+
if (newWindow) {
console.log("[PromptManager] Web interface opened in new tab");
- this.showNotification("Web interface opened in new tab", "success");
+ this.showNotification(
+ "Web interface opened in new tab",
+ "success",
+ );
} else {
// Fallback if popup was blocked
- console.log("[PromptManager] Popup blocked, trying alternative method");
+ console.log(
+ "[PromptManager] Popup blocked, trying alternative method",
+ );
window.location.href = webUrl;
}
} else {
@@ -650,7 +655,9 @@ app.registerExtension({
this.showNotification("Web interface opened in popup", "success");
} else {
// Fallback if popup was blocked
- console.log("[PromptManager] Popup blocked, trying alternative method");
+ console.log(
+ "[PromptManager] Popup blocked, trying alternative method",
+ );
window.location.href = webUrl;
}
}