Files
ComfyAssets-ComfyUI_PromptM…/web/js/scan-progress.js
T
Vito Sansevero dc192d5836 fix: output scan runs as a background job that survives the browser going away
Closing the dashboard tab or clicking outside the scan modal raised
ClientConnectionResetError out of scan_images and abandoned the scan
mid-batch, leaving a half-scanned database. Two overlapping scans could
also run at once.

- py/api/scan_job.py: ScanJob fans progress out to any number of SSE
  subscribers (latest snapshot first, then live events)
- POST /prompt_manager/scan starts the scan as an asyncio task, or attaches
  to the one already running; a write to a dead socket ends that stream
  quietly while the scan continues
- GET /prompt_manager/scan/status reports the running flag and last event
- dashboard: dismissing the modal while a scan runs minimizes it to a
  progress pill, the page re-attaches to a running scan after a reload,
  a lost stream reconnects, and leaving the page mid-scan asks first
2026-10-08 18:01:34 -07:00

110 lines
3.7 KiB
JavaScript

/**
* State of the output-image scan as seen by the dashboard.
*
* The scan runs on the server as a background job (see py/api/scan_job.py) and
* reports progress over SSE. This module turns those events into a small
* immutable state object and answers the UI questions that depend on it:
* what dismissing the modal should do, what the minimized pill should say, and
* how to rebuild the view from GET /prompt_manager/scan/status after a reload.
*
* Loadable in the browser (window.ScanProgress) and in Node for tests.
*/
(function (root) {
"use strict";
const IDLE = "idle";
const RUNNING = "running";
const DONE = "done";
const FAILED = "failed";
function initialState() {
return {
status: IDLE,
progress: 0,
statusText: "",
processed: 0,
found: 0,
added: 0,
linked: 0,
message: "",
};
}
function num(value, fallback) {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function text(value, fallback) {
return typeof value === "string" ? value : fallback;
}
/**
* @param {object} state current state (never mutated)
* @param {object} event a decoded SSE payload: {type: "progress"|"complete"|"error", ...}
* @returns {object} the next state, or `state` itself when the event changes nothing
*/
function reduceScanEvent(state, event) {
if (!event || typeof event !== "object") return state;
if (event.type === "progress") {
return {
...state,
status: RUNNING,
progress: num(event.progress, state.progress),
statusText: text(event.status, state.statusText),
processed: num(event.processed, state.processed),
found: num(event.found, state.found),
};
}
if (event.type === "complete") {
return {
...state,
status: DONE,
progress: 100,
statusText: "Scan completed!",
processed: num(event.processed, state.processed),
found: num(event.found, state.found),
added: num(event.added, state.added),
linked: num(event.linked, state.linked),
};
}
if (event.type === "error") {
return { ...state, status: FAILED, message: text(event.message, "Scan failed") };
}
return state;
}
/** Rebuild the state from the /prompt_manager/scan/status payload. */
function fromStatus(status) {
if (!status || !status.running) return initialState();
const state = reduceScanEvent(initialState(), status.last_event);
if (state.status !== IDLE) return state;
return { ...state, status: RUNNING, statusText: "Scanning..." };
}
/** Closing a running scan's modal only hides it; the scan continues. */
function dismissAction(state) {
return state && state.status === RUNNING ? "minimize" : "close";
}
/** Label for the minimized progress pill; empty when nothing is running. */
function progressLabel(state) {
if (!state || state.status !== RUNNING) return "";
const pct = Math.round(state.progress);
return `Scanning ${pct}% · ${state.processed} files · ${state.found} prompts`;
}
const api = {
initialState,
reduceScanEvent,
fromStatus,
dismissAction,
progressLabel,
IDLE,
RUNNING,
DONE,
FAILED,
};
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.ScanProgress = api;
})(typeof window !== "undefined" ? window : globalThis);