diff --git a/web/js/admin.js b/web/js/admin.js
index 405251e..d0fb25f 100644
--- a/web/js/admin.js
+++ b/web/js/admin.js
@@ -3,11 +3,8 @@
this.prompts = [];
this.listMode = "recent"; // "recent" or "search"; a sort change reloads it
this.selectedPrompts = new Set();
+ // The page's own options (Page options); every other setting lives in System.
this.settings = {
- resultTimeout: 5,
- webuiDisplayMode: 'popup',
- workerThreads: 1,
- cpuCount: 1,
infiniteScroll: false,
};
this.categories = [];
@@ -125,11 +122,8 @@
document.getElementById("bulkTagBtn").addEventListener("click", () => this.showBulkTagModal());
document.getElementById("bulkCategoryBtn").addEventListener("click", () => this.showBulkCategoryModal());
document.getElementById("exportBtn").addEventListener("click", () => this.exportPrompts());
- document.getElementById("settingsBtn").addEventListener("click", () => this.showSettingsModal());
+ document.getElementById("pageOptionsBtn").addEventListener("click", () => this.showSettingsModal());
document.getElementById("statsBtn").addEventListener("click", () => this.showStats());
- document.getElementById("maintenanceBtn").addEventListener("click", () => this.showMaintenanceModal());
- document.getElementById("backupBtn").addEventListener("click", () => this.backupDatabase());
- document.getElementById("restoreBtn").addEventListener("click", () => this.showRestoreModal());
document.getElementById("scanBtn").addEventListener("click", () => this.showScanModal());
document.getElementById("autoTagBtn").addEventListener("click", () => this.showAutoTagModal());
document.getElementById("addPromptBtn").addEventListener("click", () => this.showAddPromptModal());
@@ -193,8 +187,6 @@
// Settings modal
document.getElementById("saveSettings").addEventListener("click", () => this.saveSettings());
document.getElementById("cancelSettings").addEventListener("click", () => this.hideModal("settingsModal"));
- document.getElementById("refreshMonitoringStatus").addEventListener("click", () => this.updateMonitoringStatus());
- document.getElementById("addScanPathBtn").addEventListener("click", () => this.addScanPath());
// Bulk tag modal
document.getElementById("confirmBulkTag").addEventListener("click", () => this.confirmBulkTag());
@@ -208,14 +200,6 @@
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"));
-
- // Maintenance modal
- document.getElementById("runMaintenanceBtn").addEventListener("click", () => this.runMaintenance());
- document.getElementById("selectAllMaintenanceBtn").addEventListener("click", () => this.selectAllMaintenance());
- document.getElementById("clearAllMaintenanceBtn").addEventListener("click", () => this.clearAllMaintenance());
// Scan modal
document.getElementById("startScan").addEventListener("click", () => this.startScan());
@@ -249,7 +233,6 @@
document.getElementById('wd14GeneralThreshold').addEventListener('input', (e) => {
document.getElementById('wd14GeneralThresholdValue').textContent = parseFloat(e.target.value).toFixed(2);
});
- document.getElementById("workerThreads").addEventListener("input", () => this.renderWorkerThreads());
document.getElementById('wd14CharacterThreshold').addEventListener('input', (e) => {
document.getElementById('wd14CharacterThresholdValue').textContent = parseFloat(e.target.value).toFixed(2);
});
@@ -298,21 +281,9 @@
if (response.ok) {
const data = await response.json();
if (data.success && data.settings) {
- this.settings.resultTimeout = data.settings.result_timeout ?? 5;
- this.settings.webuiDisplayMode = data.settings.webui_display_mode || 'popup';
this.settings.infiniteScroll = data.settings.infinite_scroll === true;
// The saved default seeds the session until the header checkbox is used
if (!this.infiniteScrollChosen) this.setInfiniteScroll(this.settings.infiniteScroll, { session: false });
- this.settings.cpuCount = Math.max(1, parseInt(data.settings.cpu_count, 10) || 1);
- this.settings.workerThreads = Math.min(
- this.settings.cpuCount, Math.max(1, parseInt(data.settings.worker_threads, 10) || 1)
- );
- this.settings.monitoredDirectories = data.settings.monitored_directories || [];
- this.settings.galleryRootPaths = data.settings.gallery_root_paths || [];
- // Backward compat: if server only returned old field
- if (!this.settings.galleryRootPaths.length && data.settings.gallery_root_path) {
- this.settings.galleryRootPaths = [data.settings.gallery_root_path];
- }
}
}
} catch (error) {
@@ -996,255 +967,27 @@
});
}
- renderWorkerThreads() {
- const threads = document.getElementById("workerThreads");
- document.getElementById("workerThreadsValue").textContent =
- `${threads.value} / ${this.settings.cpuCount} cores`;
- }
-
+ /** Page options: the prompt list's saved Infinite Scrolling default (scan folders and the rest are in System). */
showSettingsModal() {
- document.getElementById("resultTimeout").value = this.settings.resultTimeout;
- document.getElementById("webuiDisplayMode").value = this.settings.webuiDisplayMode;
document.getElementById("infiniteScrollDefault").checked = this.settings.infiniteScroll;
- const threads = document.getElementById("workerThreads");
- threads.max = this.settings.cpuCount;
- threads.value = this.settings.workerThreads;
- this.renderWorkerThreads();
- this.renderScanPaths();
- this.updateMonitoringStatus();
- this.detectLoraManager();
this.showModal("settingsModal");
}
- async updateMonitoringStatus() {
- const statusEl = document.getElementById("monitoringStatus");
- try {
- const response = await ApiClient.fetchUrl("/prompt_manager/settings");
- if (response.ok) {
- const data = await response.json();
- const dirs = data.settings?.monitored_directories || [];
- if (dirs.length > 0) {
- statusEl.innerHTML = dirs.map(d => `
✓ ${escapeHtml(d)}
`).join('');
- } else {
- statusEl.textContent = 'No directories being monitored (auto-detect on restart)';
- }
- }
- } catch (error) {
- statusEl.textContent = 'Unable to fetch status';
- }
- }
-
- // ── LoraManager integration ──────────────────────────────
-
- async detectLoraManager() {
- const badge = document.getElementById("loraDetectionBadge");
- const toggle = document.getElementById("loraEnabled");
- const settings = document.getElementById("loraSettings");
-
- try {
- // First check current status
- const statusRes = await ApiClient.fetchUrl("/prompt_manager/lora/status");
- if (statusRes.ok) {
- const status = await statusRes.json();
- if (status.success && status.enabled) {
- badge.textContent = "enabled";
- badge.className = "text-[10px] px-1.5 py-0.5 rounded-full bg-green-500/20 text-green-400";
- toggle.checked = true;
- toggle.disabled = false;
- document.getElementById("loraManagerPath").value = status.path;
- document.getElementById("loraTriggerWords").checked = status.trigger_words_enabled;
- const keyInput = document.getElementById("civitaiApiKey");
- keyInput.value = "";
- keyInput.placeholder = status.has_civitai_api_key
- ? "A key is stored — leave empty to keep it"
- : "Optional — required for NSFW example images";
- document.getElementById("clearCivitaiKey").checked = false;
- settings.classList.remove("hidden");
- this._loraPath = status.path;
- this._bindLoraEvents();
- return;
- }
- }
-
- // Try auto-detection
- const detectRes = await ApiClient.fetchUrl("/prompt_manager/lora/detect");
- if (!detectRes.ok) return;
- const data = await detectRes.json();
-
- if (data.detected) {
- badge.textContent = "detected";
- badge.className = "text-[10px] px-1.5 py-0.5 rounded-full bg-blue-500/20 text-blue-400";
- toggle.disabled = false;
- document.getElementById("loraManagerPath").value = data.path;
- this._loraPath = data.path;
- } else {
- badge.textContent = "not installed";
- badge.className = "text-[10px] px-1.5 py-0.5 rounded-full bg-pm-input text-pm-muted";
- toggle.disabled = true;
- }
-
- this._bindLoraEvents();
- } catch (e) {
- badge.textContent = "error";
- badge.className = "text-[10px] px-1.5 py-0.5 rounded-full bg-red-500/20 text-red-400";
- }
- }
-
- _bindLoraEvents() {
- if (this._loraBound) return;
- this._loraBound = true;
-
- const toggle = document.getElementById("loraEnabled");
- const settings = document.getElementById("loraSettings");
-
- toggle.addEventListener("change", () => {
- if (toggle.checked) {
- settings.classList.remove("hidden");
- } else {
- settings.classList.add("hidden");
- }
- });
-
- document.getElementById("loraImportBtn").addEventListener("click", () => {
- this.runLoraImport();
- });
- }
-
- async saveLoraSettings() {
- const enabled = document.getElementById("loraEnabled").checked;
- const triggerWords = document.getElementById("loraTriggerWords").checked;
- const civitaiKey = document.getElementById("civitaiApiKey").value.trim();
- const clearKey = document.getElementById("clearCivitaiKey").checked;
- const path = this._loraPath || "";
-
- // The server never returns the stored key: an empty field keeps it, the
- // checkbox removes it, and a non-empty field replaces it
- const payload = { enabled, path, trigger_words_enabled: triggerWords };
- if (clearKey) payload.clear_civitai_api_key = true;
- else if (civitaiKey) payload.civitai_api_key = civitaiKey;
-
- try {
- const res = await ApiClient.fetchUrl("/prompt_manager/lora/enable", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload),
- });
- const data = await res.json();
- if (!data.success) {
- this.showNotification(data.error || "Failed to save LoRA settings", "error");
- return false;
- }
- return true;
- } catch (e) {
- this.showNotification("Failed to save LoRA settings", "error");
- return false;
- }
- }
-
- async runLoraImport() {
- const saved = await this.saveLoraSettings();
- if (!saved) return;
-
- // Close settings and show progress modal
- this.hideModal("settingsModal");
- document.getElementById("loraImportStatus").textContent = "Initializing...";
- document.getElementById("loraImportBar").style.width = "0%";
- document.getElementById("loraImportPercent").textContent = "0%";
- document.getElementById("loraImportProcessed").textContent = "0 processed";
- document.getElementById("loraImportImported").textContent = "0 imported";
- this.showModal("loraImportModal");
-
- try {
- const res = await ApiClient.fetchUrl("/prompt_manager/lora/scan", { method: "POST" });
- const reader = res.body.getReader();
- const decoder = new TextDecoder();
- let buffer = "";
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n\n");
- buffer = lines.pop();
-
- for (const line of lines) {
- if (!line.startsWith("data: ")) continue;
- try {
- const data = JSON.parse(line.slice(6));
- const pct = data.progress || 0;
- document.getElementById("loraImportBar").style.width = `${pct}%`;
- document.getElementById("loraImportPercent").textContent = `${pct}%`;
-
- if (data.status) {
- document.getElementById("loraImportStatus").textContent = data.status;
- }
- if (data.processed !== undefined) {
- document.getElementById("loraImportProcessed").textContent = `${data.processed} processed`;
- }
- if (data.imported !== undefined) {
- document.getElementById("loraImportImported").textContent = `${data.imported} imported`;
- }
-
- if (data.type === "complete") {
- document.getElementById("loraImportStatus").textContent =
- `Done — ${data.imported} imported, ${data.skipped} skipped`;
- this.loadStatistics();
- setTimeout(() => this.hideModal("loraImportModal"), 2000);
- } else if (data.type === "error") {
- // A refusal (another job runs) or a failed import: name it and close the modal.
- document.getElementById("loraImportStatus").textContent = `Error: ${data.message}`;
- this.showNotification(`LoRA import failed: ${data.message}`, "error");
- setTimeout(() => this.hideModal("loraImportModal"), 3000);
- }
- } catch (e) { /* skip malformed SSE */ }
- }
- }
- } catch (e) {
- document.getElementById("loraImportStatus").textContent = `Error: ${e.message}`;
- setTimeout(() => this.hideModal("loraImportModal"), 3000);
- }
- }
-
+ /** Saves Page options: only `infinite_scroll`, so a stale page never overwrites what System saved (#330 S13-9). */
async saveSettings() {
- const timeout = parseInt(document.getElementById("resultTimeout").value);
- const displayMode = document.getElementById("webuiDisplayMode").value;
- const workerThreads = parseInt(document.getElementById("workerThreads").value, 10) || 1;
const infiniteScroll = document.getElementById("infiniteScrollDefault").checked;
- const galleryPaths = this._collectScanPaths().filter(p => p !== '');
-
- this.settings.resultTimeout = timeout;
- this.settings.webuiDisplayMode = displayMode;
- this.settings.workerThreads = workerThreads;
- this.settings.infiniteScroll = infiniteScroll;
- this.settings.galleryRootPaths = galleryPaths;
try {
const response = await ApiClient.fetchUrl("/prompt_manager/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- result_timeout: timeout,
- webui_display_mode: displayMode,
- worker_threads: workerThreads,
- infinite_scroll: infiniteScroll,
- gallery_root_paths: galleryPaths
- }),
+ body: JSON.stringify({ infinite_scroll: infiniteScroll }),
});
if (response.ok) {
- const data = await response.json();
-
- // Save LoRA integration settings (fire-and-forget)
- await this.saveLoraSettings();
-
+ this.settings.infiniteScroll = infiniteScroll;
this.setInfiniteScroll(infiniteScroll, { session: false });
-
- if (data.restart_required) {
- this.showNotification("Settings saved. Restart ComfyUI for gallery path changes to take effect.", "warning");
- } else {
- this.showNotification("Settings saved successfully", "success");
- }
+ this.showNotification("Page options saved", "success");
this.hideModal("settingsModal");
} else {
// A refusal names its reason (a corrupt config.json, a changed file): show it.
@@ -1257,57 +1000,6 @@
}
}
- renderScanPaths() {
- const container = document.getElementById("scanPathsList");
- const paths = this.settings.galleryRootPaths || [];
- container.textContent = ''; // Clear safely
-
- const toRender = paths.length > 0 ? paths : [''];
- toRender.forEach((p, i) => {
- const row = document.createElement('div');
- row.className = 'flex gap-2 items-center';
- row.dataset.pathIndex = i;
-
- const input = document.createElement('input');
- input.type = 'text';
- input.value = p;
- input.placeholder = 'Leave empty for auto-detect';
- input.className = 'flex-1 px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] focus:outline-none scan-path-input';
-
- const removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- removeBtn.className = 'text-pm-muted hover:text-red-400 p-1 text-lg leading-none';
- removeBtn.title = 'Remove';
- removeBtn.textContent = '\u00d7'; // multiplication sign as X
- removeBtn.addEventListener('click', () => this.removeScanPath(i));
-
- row.appendChild(input);
- row.appendChild(removeBtn);
- container.appendChild(row);
- });
- }
-
- addScanPath() {
- const paths = this._collectScanPaths();
- paths.push('');
- this.settings.galleryRootPaths = paths;
- this.renderScanPaths();
- const inputs = document.querySelectorAll('.scan-path-input');
- if (inputs.length) inputs[inputs.length - 1].focus();
- }
-
- removeScanPath(index) {
- const paths = this._collectScanPaths();
- paths.splice(index, 1);
- this.settings.galleryRootPaths = paths;
- this.renderScanPaths();
- }
-
- _collectScanPaths() {
- return Array.from(document.querySelectorAll('.scan-path-input'))
- .map(input => input.value.trim());
- }
-
toggleSelectAll(checked) {
document.querySelectorAll(".prompt-checkbox").forEach((checkbox) => {
checkbox.checked = checked;
@@ -1788,287 +1480,6 @@
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
- // Maintenance functionality
- showMaintenanceModal() {
- this.showModal("maintenanceModal");
- }
-
- closeMaintenance() {
- this.hideModal("maintenanceModal");
- }
-
- selectAllMaintenance() {
- document.querySelectorAll('.maintenance-option').forEach(checkbox => {
- checkbox.checked = true;
- });
- }
-
- clearAllMaintenance() {
- document.querySelectorAll('.maintenance-option').forEach(checkbox => {
- checkbox.checked = false;
- });
- }
-
- async runMaintenance() {
- const resultsContainer = document.getElementById("maintenanceResults");
- const runButton = document.getElementById("runMaintenanceBtn");
-
- // Get selected operations
- const selectedOps = Array.from(document.querySelectorAll('.maintenance-option:checked'))
- .map(checkbox => checkbox.id);
-
- if (selectedOps.length === 0) {
- this.showNotification('Please select at least one maintenance operation', 'warning');
- return;
- }
-
- // Show loading state
- runButton.disabled = true;
- runButton.innerHTML = '🔄 Running...';
- resultsContainer.innerHTML = `
-
-
-
Running maintenance operations...
-
Operations: ${selectedOps.map(op => op.replace(/_/g, ' ')).join(', ')}
-
- `;
-
- try {
- const response = await ApiClient.fetchUrl('/prompt_manager/maintenance', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ operations: selectedOps })
- });
-
- if (response.ok) {
- const data = await response.json();
- if (data.success) {
- this.renderMaintenanceResults(data);
-
- // Show overall success notification
- if (data.all_successful) {
- this.showNotification('✅ All maintenance operations completed successfully!', 'success');
- } else {
- this.showNotification('⚠️ Some maintenance operations had issues', 'warning');
- }
-
- // Refresh stats after maintenance
- setTimeout(() => this.loadStatistics(), 1000);
- } else {
- throw new Error(data.error || 'Maintenance failed');
- }
- } else {
- // A refusal names the job that runs ("Scan for metadata already running").
- const data = await response.json().catch(() => ({}));
- throw new Error(data.error || `HTTP ${response.status}: ${response.statusText}`);
- }
- } catch (error) {
- console.error('Maintenance error:', error);
- resultsContainer.innerHTML = `
-
-
❌ Maintenance Failed
-
${escapeHtml(error.message)}
-
- `;
- this.showNotification('❌ Maintenance failed', 'error');
- } finally {
- runButton.disabled = false;
- runButton.innerHTML = '🔧 Run Maintenance';
- }
- }
-
- renderMaintenanceResults(data) {
- const resultsContainer = document.getElementById("maintenanceResults");
-
- let html = '
';
-
- // Summary
- html += '
';
- html += '
📋 Maintenance Summary
';
- html += '
';
- html += `
-
- Operations Completed
- ${escapeHtml(data.operations_completed)}
-
- `;
- html += `
-
- Overall Success
-
- ${data.all_successful ? '✅ ALL PASSED' : '⚠️ SOME ISSUES'}
-
-
- `;
- html += '
';
-
- // Detailed results
- for (const [operation, result] of Object.entries(data.results)) {
- const bgColor = result.success ? 'bg-pm-success-tint border-pm-success' : 'bg-pm-error-tint border-pm-error';
- const statusIcon = result.success ? '✅' : '❌';
- const statusText = result.success ? 'SUCCESS' : 'FAILED';
-
- html += `
`;
- html += `
`;
- html += `${statusIcon}`;
- html += `${escapeHtml(operation.replace(/_/g, ' '))}`;
- html += `${statusText}`;
- html += `
`;
-
- if (result.message) {
- html += `
${escapeHtml(result.message)}
`;
- }
-
- // Show specific details
- if (result.removed_count !== undefined) {
- html += `
Items removed: ${escapeHtml(result.removed_count)}
`;
- }
-
- if (result.duplicate_hashes !== undefined) {
- html += `
Duplicate hash groups found: ${escapeHtml(result.duplicate_hashes)}
`;
- }
-
- if (result.issues_found !== undefined) {
- html += `
Issues found: ${escapeHtml(result.issues_found)}
`;
- if (result.issues && result.issues.length > 0) {
- html += `
`;
- result.issues.forEach(issue => {
- html += `- ${escapeHtml(issue)}
`;
- });
- html += `
`;
- }
- }
-
- if (result.info) {
- html += `
`;
- html += `
Total prompts: ${escapeHtml(result.info.total_prompts || 'N/A')}
`;
- html += `
Database size: ${escapeHtml(result.info.database_size_bytes ? this.formatFileSize(result.info.database_size_bytes) : 'N/A')}
`;
- html += `
`;
- }
-
- if (result.error) {
- html += `
`;
- html += `Error: ${escapeHtml(result.error)}`;
- html += `
`;
- }
-
- html += '
';
- }
-
- html += '
';
- resultsContainer.innerHTML = html;
- }
-
- // Database backup and restore functionality
- async backupDatabase() {
- try {
- const response = await ApiClient.fetchUrl('/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 ApiClient.fetchUrl('/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 the restored data
- await this.loadInitialData();
- } 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