@@ -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)
|
||||
|
||||
+160
@@ -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
|
||||
</button>
|
||||
<button id="backupBtn"
|
||||
class="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
💾 Backup DB
|
||||
</button>
|
||||
<button id="restoreBtn"
|
||||
class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
📁 Restore DB
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,6 +305,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Restore Modal -->
|
||||
<div id="restoreModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full mx-4 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-gray-100 mb-6">📁 Restore Database</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="bg-yellow-900/20 border border-yellow-600 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-yellow-400 text-xl">⚠️</span>
|
||||
<div>
|
||||
<h4 class="text-yellow-400 font-medium">Warning</h4>
|
||||
<p class="text-gray-300 text-sm">This will replace your current database. A backup will be created automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Select Database File (.db)</label>
|
||||
<input type="file" id="restoreFileInput" accept=".db"
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-100 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button id="cancelRestore"
|
||||
class="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="confirmRestore"
|
||||
class="px-6 py-2 bg-amber-600 hover:bg-amber-700 text-white font-medium rounded-lg transition-colors">
|
||||
Restore Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagnostics Modal -->
|
||||
<div id="diagnosticsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl max-w-4xl w-full max-h-[90vh] overflow-hidden mx-4 border border-gray-700">
|
||||
@@ -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
|
||||
|
||||
+20
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user