From b2fb630f616a8c92e2f6112be0e6a90648e5777e Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 7 Oct 2026 19:00:15 -0700 Subject: [PATCH] fix: follow the POST-only autotag streams and the new LoRA and list contracts autotag/download and autotag/start are POST-only now; EventSource can only GET, so both pages stream their progress through SseStream.connect(url, {method: 'POST'}), which keeps the onmessage/onerror/close surface the handlers already use. autotag/single is called with image_id and the re-tag dialog shows the server's filename instead of splitting an absolute path. lora/status reports has_civitai_api_key instead of the key: the field shows a 'key is stored' placeholder, an empty field keeps the key on save and a 'Clear the stored key on save' checkbox sends clear_civitai_api_key. Thumbnail generation reports per-file failures as file_error events; both progress views count them (and show error_count from progress) without closing the stream. The auto-tag review follows pagination.has_more on the capped /images/all endpoint, and /search is paged like /recent so paging in search mode no longer switches the list back to recent prompts. --- web/js/admin.js | 70 +++++++++++++++++++++++++++++++++-------------- web/js/gallery.js | 36 ++++++++++++++++++++---- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/web/js/admin.js b/web/js/admin.js index 4e87400a..255a6816 100644 --- a/web/js/admin.js +++ b/web/js/admin.js @@ -452,7 +452,7 @@ } } - async search() { + async search(page = 1) { const searchText = document.getElementById("searchText").value; const category = document.getElementById("searchCategory").value; const tags = document.getElementById("searchTags").value; @@ -467,21 +467,32 @@ if (tags) params.append("tags", tags); const folder = document.getElementById("searchFolder").value; if (folder) params.append("folder", folder); - params.append("limit", "100"); + const limit = this.pagination.limit; + params.append("limit", String(limit)); + params.append("offset", String((page - 1) * limit)); + params.append("page", String(page)); this.listMode = "search"; const url = `/prompt_manager/search?${PromptListSort.withSort(params, this.currentSort())}`; const data = await this.listRequest((signal) => this.api.get(url, { signal })); if (data === ApiClient.STALE) return; // a newer list request owns the loading state + const reported = data.pagination || {}; + const next = ListState.nextPageState( + { page: this.pagination.currentPage, limit, total: this.pagination.total }, + { page: reported.page ?? page, total: reported.total ?? data.results.length }, + ); this.prompts = data.results; + this.pagination = { ...this.pagination, currentPage: next.page, total: next.total, totalPages: next.totalPages }; this.renderPrompts(); + this.updatePaginationControls(); document.getElementById("resultsTitle").textContent = "Search Results"; } catch (error) { this.showNotification("Search failed", "error"); console.error("Search error:", error); } this.showListLoaded(); + document.getElementById("paginationControls").classList.remove("hidden"); } currentSort() { @@ -495,7 +506,8 @@ /** Re-fetch the page currently shown, e.g. after a prompt was edited, tagged or deleted. */ refreshList() { - return this.listMode === "search" ? this.search() : this.loadRecentPrompts(this.pagination.currentPage); + const page = this.pagination.currentPage; + return this.listMode === "search" ? this.search(page) : this.loadRecentPrompts(page); } handleActionClick(e) { @@ -828,7 +840,12 @@ toggle.disabled = false; document.getElementById("loraManagerPath").value = status.path; document.getElementById("loraTriggerWords").checked = status.trigger_words_enabled; - document.getElementById("civitaiApiKey").value = status.civitai_api_key || ""; + 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(); @@ -884,18 +901,20 @@ 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 fetch("/prompt_manager/lora/enable", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - enabled, - path, - trigger_words_enabled: triggerWords, - civitai_api_key: civitaiKey, - }), + body: JSON.stringify(payload), }); const data = await res.json(); if (!data.success) { @@ -2467,15 +2486,14 @@ if (page < 1 || page > this.pagination.totalPages || page === this.pagination.currentPage) { return; } - await this.loadRecentPrompts(page); + await (this.listMode === "search" ? this.search(page) : this.loadRecentPrompts(page)); // Scroll to top of results after page loads window.scrollTo({ top: 0, behavior: 'smooth' }); } async changeLimit(newLimit) { this.pagination.limit = newLimit; - this.pagination.currentPage = 1; // Reset to first page - await this.loadRecentPrompts(1); + await this.reloadPrompts(); // back to the first page of the current list } // Metadata functionality @@ -3401,7 +3419,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; this.showModal("autoTagDownloadModal"); try { - this.autoTagState.downloadEventSource = new EventSource(`/prompt_manager/autotag/download/${modelType}`); + this.autoTagState.downloadEventSource = SseStream.connect(`/prompt_manager/autotag/download/${modelType}`, { method: 'POST' }); this.autoTagState.downloadEventSource.onmessage = (event) => { const data = JSON.parse(event.data); @@ -3487,7 +3505,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; formData.append('prompt', prompt); } - this.autoTagState.eventSource = new EventSource(`/prompt_manager/autotag/start?${formData.toString()}`); + this.autoTagState.eventSource = SseStream.connect(`/prompt_manager/autotag/start?${formData.toString()}`, { method: 'POST' }); this.autoTagState.eventSource.onmessage = (event) => { const data = JSON.parse(event.data); @@ -3561,10 +3579,9 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // Get ALL images with linked prompts from the database try { - const scanResponse = await fetch('/prompt_manager/images/all'); - const scanData = await scanResponse.json(); + const scanData = { images: await this.fetchAllImages() }; - if (!scanData.success || !scanData.images || scanData.images.length === 0) { + if (scanData.images.length === 0) { this.hideModal("autoTagLoadingModal"); this.showNotification('No images with linked prompts found in database', 'warning'); return; @@ -3611,7 +3628,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // Set up the modal content document.getElementById('retagPreviewImage').src = imageUrl; - document.getElementById('retagImageName').textContent = image.image_path.split('/').pop(); + document.getElementById('retagImageName').textContent = image.filename || String(image.image_path || '').split(/[\\/]/).pop(); // Display existing tags const tagsContainer = document.getElementById('retagExistingTags'); @@ -3690,7 +3707,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; try { const requestBody = { - image_path: image.image_path, + image_id: image.id, model_type: this.autoTagState.modelType, }; if (this.autoTagState.modelType.startsWith('wd14')) { @@ -3724,6 +3741,19 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; } } + /** /prompt_manager/images/all is capped per call; collect every page. */ + async fetchAllImages() { + const images = []; + let offset = 0; + for (;;) { + const data = await this.api.get(`/prompt_manager/images/all?offset=${offset}`); + const page = Array.isArray(data.images) ? data.images : []; + images.push(...page); + if (!ListState.hasMorePages(data, page.length)) return images; + offset += page.length; + } + } + renderReviewTags() { const visibleContainer = document.getElementById('reviewTagsVisible'); const hiddenContainer = document.getElementById('reviewTagsHidden'); diff --git a/web/js/gallery.js b/web/js/gallery.js index 04850d93..ebcdb28e 100644 --- a/web/js/gallery.js +++ b/web/js/gallery.js @@ -1951,7 +1951,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // More detailed progress information const elapsedText = data.elapsed ? ` | Time: ${Math.floor(data.elapsed)}s` : ''; - progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped} | Rate: ${data.rate} img/s${elapsedText}`; + const failedText = data.error_count ? `, Failed: ${data.error_count}` : ''; + progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped}${failedText} | Rate: ${data.rate} img/s${elapsedText}`; // Update ETA if (data.eta > 0 && data.eta < 3600) { @@ -2003,6 +2004,17 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; resolve(resultData); }); + // Per-file failures: count them, keep listening + eventSource.addEventListener('file_error', (event) => { + if (cancelled) return; + try { + const data = JSON.parse(event.data); + console.warn('Thumbnail failed:', data.current_file || data.file || '', data.error || ''); + } catch (e) { + console.warn('Thumbnail failed (unparseable event)'); + } + }); + // Handle errors eventSource.addEventListener('error', (event) => { if (cancelled) return; @@ -2676,7 +2688,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // Update details const elapsedText = data.elapsed ? ` | Time: ${Math.floor(data.elapsed)}s` : ''; - progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped} | Rate: ${data.rate} img/s${elapsedText}`; + const failedText = data.error_count ? `, Failed: ${data.error_count}` : ''; + progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped}${failedText} | Rate: ${data.rate} img/s${elapsedText}`; // Update ETA if (data.eta > 0 && data.eta < 3600) { @@ -2731,6 +2744,19 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; resolve(resultData); }); + // Per-file failures: report them, keep listening + eventSource.addEventListener('file_error', (event) => { + if (isCancelled()) return; + let detail = 'Thumbnail failed'; + try { + const data = JSON.parse(event.data); + detail = `Thumbnail failed: ${data.current_file || data.file || ''} ${data.error || ''}`.trim(); + } catch (e) { + // non-JSON frame; keep the generic message + } + addStatusMessage(detail, 'error'); + }); + // Handle errors eventSource.addEventListener('error', (event) => { if (isCancelled()) { @@ -3183,7 +3209,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; this.showModal("autoTagDownloadModal"); try { - this.autoTagState.downloadEventSource = new EventSource(`/prompt_manager/autotag/download/${modelType}`); + this.autoTagState.downloadEventSource = SseStream.connect(`/prompt_manager/autotag/download/${modelType}`, { method: 'POST' }); this.autoTagState.downloadEventSource.onmessage = (event) => { const data = JSON.parse(event.data); @@ -3295,7 +3321,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; formData.append('prompt', prompt); } - this.autoTagState.eventSource = new EventSource(`/prompt_manager/autotag/start?${formData.toString()}`); + this.autoTagState.eventSource = SseStream.connect(`/prompt_manager/autotag/start?${formData.toString()}`, { method: 'POST' }); this.autoTagState.eventSource.onmessage = (event) => { const data = JSON.parse(event.data); @@ -3397,7 +3423,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; try { const requestBody = { - image_path: image.path, + image_id: image.id, model_type: this.autoTagState.modelType, }; if (this.autoTagState.modelType.startsWith('wd14')) {