From 7206b454db9972499bf082d4bbc5efb617654c97 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 26 Apr 2026 04:23:37 +0100 Subject: [PATCH] Make queue UI optional and fix idle summary --- frontend/src/app.ts | 82 +++++++++++++++++---------- scripts/run_gui.ps1 | 7 ++- scripts/run_gui.py | 15 ++++- src/repixelizer/gui.py | 4 ++ src/repixelizer/gui_static/app.js | 80 ++++++++++++++++---------- src/repixelizer/gui_static/index.html | 2 +- tests/test_gui.py | 39 +++++++++++++ 7 files changed, 165 insertions(+), 64 deletions(-) diff --git a/frontend/src/app.ts b/frontend/src/app.ts index ec763f9..863292c 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -56,6 +56,7 @@ type RuntimeConfig = { ui: { showDeviceControl: boolean; showStripBackgroundControl: boolean; + showQueuePanel: boolean; }; }; @@ -213,6 +214,7 @@ const phaseYInput = byId("phaseYInput"); const stepsInput = byId("stepsInput"); const seedInput = byId("seedInput"); const runControlsCopy = byId("runControlsCopy"); +const queuePanel = byId("queuePanel"); const queueSummaryPanel = byId("queueSummary"); const cancelJobButton = byId("cancelJobButton"); const deviceField = byId("deviceField"); @@ -361,6 +363,11 @@ function renderEventLog(): void { } function renderQueueSummary(): void { + if (!state.runtimeConfig?.ui.showQueuePanel) { + queueSummaryPanel.innerHTML = ""; + cancelJobButton.hidden = true; + return; + } queueSummaryPanel.innerHTML = ""; if (hasActiveJob()) { const cards: Array<[string, string]> = [ @@ -436,6 +443,16 @@ function hasActiveJob(): boolean { return state.jobId !== null && ["queued", "running", "waiting"].includes(state.status); } +function queueSummaryFromState(activeStatus: string | null = state.status): QueueSummary { + return { + queueDepth: state.queueDepth, + waitingCount: state.waitingCount, + queueCapacity: state.queueCapacity || (state.runtimeConfig?.limits.queueCapacity ?? 0), + hasActiveJob: state.queueDepth > 0, + activeStatus, + }; +} + function formatByteLimit(bytes: number): string { if (bytes >= 1024 * 1024) { return `${formatNumber(bytes / (1024 * 1024), 1)} MiB`; @@ -473,6 +490,7 @@ function applyRuntimeConfig(config: RuntimeConfig): void { state.runtimeConfig = config; deviceField.hidden = !config.ui.showDeviceControl; stripBackgroundField.hidden = !config.ui.showStripBackgroundControl; + queuePanel.hidden = !config.ui.showQueuePanel; stepsInput.max = String(config.limits.maxSteps); if (stepsInput.value.trim() === "" || Number(stepsInput.value) > config.limits.maxSteps || Number(stepsInput.value) === 48) { stepsInput.value = String(config.limits.defaultSteps); @@ -747,15 +765,20 @@ function renderStatusMetrics(): void { const items: StatusMetricItem[] = []; const frame = getSelectedFrame(); if (state.stageKey === "queued" || state.status === "queued" || state.status === "waiting") { - items.push({ - label: "Line", - value: - state.queuePosition === null || state.queueDepth <= 0 - ? "pending" - : `${state.queuePosition} / ${Math.max(state.queueDepth, state.queuePosition)}`, - }); - items.push({ label: "Waiting", value: `${state.waitingCount} / ${state.queueCapacity || (state.runtimeConfig?.limits.queueCapacity ?? 0)}` }); - items.push({ label: "Heartbeat", value: `${state.heartbeatIntervalSeconds}s` }); + if (state.runtimeConfig?.ui.showQueuePanel) { + items.push({ + label: "Line", + value: + state.queuePosition === null || state.queueDepth <= 0 + ? "pending" + : `${state.queuePosition} / ${Math.max(state.queueDepth, state.queuePosition)}`, + }); + items.push({ + label: "Waiting", + value: `${state.waitingCount} / ${state.queueCapacity || (state.runtimeConfig?.limits.queueCapacity ?? 0)}`, + }); + items.push({ label: "Heartbeat", value: `${state.heartbeatIntervalSeconds}s` }); + } } else if (state.stageKey === "inference") { if (state.latticeSearch) { items.push({ label: "Sizes", value: `${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}` }); @@ -1385,6 +1408,11 @@ async function loadRuntimeConfig(): Promise { } async function refreshQueueSummary(): Promise { + if (!state.runtimeConfig?.ui.showQueuePanel) { + state.queueSummary = null; + renderQueueSummary(); + return; + } if (hasActiveJob()) { return; } @@ -1404,6 +1432,10 @@ function stopQueuePolling(): void { } function syncQueuePolling(): void { + if (!state.runtimeConfig?.ui.showQueuePanel) { + stopQueuePolling(); + return; + } if (hasActiveJob()) { stopQueuePolling(); return; @@ -1555,13 +1587,7 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise); - state.queueSummary = { - queueDepth: state.queueDepth, - waitingCount: state.waitingCount, - queueCapacity: state.queueCapacity, - hasActiveJob: state.queueDepth > 0, - activeStatus: state.status, - }; + state.queueSummary = queueSummaryFromState(); break; case "job_canceled": stopHeartbeat(); @@ -1570,8 +1596,8 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise undefined); syncQueuePolling(); - void refreshQueueSummary().catch(() => undefined); break; case "job_failed": stopHeartbeat(); @@ -1580,8 +1606,8 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise undefined); syncQueuePolling(); - void refreshQueueSummary().catch(() => undefined); break; case "stage_started": setJobState("running"); @@ -1795,8 +1821,8 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise undefined); syncQueuePolling(); - void refreshQueueSummary().catch(() => undefined); break; default: break; @@ -1852,13 +1878,7 @@ async function startRun(): Promise { const payload = (await response.json()) as JobStateSnapshot; state.jobId = payload.jobId; applyJobSnapshot(payload); - state.queueSummary = { - queueDepth: payload.queueDepth, - waitingCount: payload.waitingCount, - queueCapacity: payload.queueCapacity, - hasActiveJob: payload.queueDepth > 0, - activeStatus: payload.status, - }; + state.queueSummary = queueSummaryFromState(payload.status); connectEventStream(payload.jobId); syncHeartbeat(); syncQueuePolling(); @@ -2133,10 +2153,12 @@ async function initializeApp(): Promise { } catch (error) { addLog("Config", error instanceof Error ? error.message : "Failed to load runtime config."); } - try { - await refreshQueueSummary(); - } catch (error) { - addLog("Queue", error instanceof Error ? error.message : "Failed to load queue summary."); + if (state.runtimeConfig?.ui.showQueuePanel) { + try { + await refreshQueueSummary(); + } catch (error) { + addLog("Queue", error instanceof Error ? error.message : "Failed to load queue summary."); + } } syncQueuePolling(); await renderEverything(); diff --git a/scripts/run_gui.ps1 b/scripts/run_gui.ps1 index 118676b..79a180e 100644 --- a/scripts/run_gui.ps1 +++ b/scripts/run_gui.ps1 @@ -1,7 +1,9 @@ param( [string]$BindHost = "127.0.0.1", [int]$Port = 8000, - [switch]$Reload + [switch]$Reload, + [ValidateSet("auto", "show", "hide")] + [string]$QueueUI = "auto" ) $ErrorActionPreference = "Stop" @@ -15,7 +17,8 @@ if (-not (Test-Path $python)) { $args = @( (Join-Path $repoRoot "scripts\run_gui.py"), "--host", $BindHost, - "--port", $Port + "--port", $Port, + "--queue-ui", $QueueUI ) if ($Reload) { diff --git a/scripts/run_gui.py b/scripts/run_gui.py index b93d2aa..48ebce0 100644 --- a/scripts/run_gui.py +++ b/scripts/run_gui.py @@ -164,15 +164,28 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--host", default="127.0.0.1", help="Host interface to bind") parser.add_argument("--port", type=int, default=8000, help="Port to bind") parser.add_argument("--reload", action="store_true", help="Enable autoreload while developing the GUI server") + parser.add_argument( + "--queue-ui", + choices=("auto", "show", "hide"), + default="auto", + help="Whether to show the queue panel in the frontend. auto follows hosted-demo mode.", + ) return parser def main(argv: list[str] | None = None) -> int: repo_root = _repo_root() _ensure_src_on_path() - from repixelizer.gui import main as gui_main args = build_parser().parse_args(argv) + if args.queue_ui == "show": + os.environ["REPIXELIZER_SHOW_QUEUE_PANEL"] = "1" + elif args.queue_ui == "hide": + os.environ["REPIXELIZER_SHOW_QUEUE_PANEL"] = "0" + else: + os.environ.pop("REPIXELIZER_SHOW_QUEUE_PANEL", None) + from repixelizer.gui import main as gui_main + try: message = _reclaim_stale_gui_port(args.host, args.port, repo_root) except RuntimeError as exc: diff --git a/src/repixelizer/gui.py b/src/repixelizer/gui.py index d391743..280ef46 100644 --- a/src/repixelizer/gui.py +++ b/src/repixelizer/gui.py @@ -148,6 +148,7 @@ def _env_int(name: str, default: int) -> int: @dataclass(slots=True) class HostedDemoConfig: hosted_demo: bool + show_queue_panel: bool max_upload_bytes: int max_input_dimension: int max_output_dimension: int @@ -180,6 +181,7 @@ class HostedDemoConfig: spool_dir = Path(tempfile.gettempdir()) / "repixelizer-gui-spool" return cls( hosted_demo=hosted_demo, + show_queue_panel=_env_flag("REPIXELIZER_SHOW_QUEUE_PANEL", hosted_demo), max_upload_bytes=max(1, _env_int("REPIXELIZER_MAX_UPLOAD_BYTES", defaults["max_upload_bytes"])), max_input_dimension=max(1, _env_int("REPIXELIZER_MAX_INPUT_DIMENSION", defaults["max_input_dimension"])), max_output_dimension=max(1, _env_int("REPIXELIZER_MAX_OUTPUT_DIMENSION", defaults["max_output_dimension"])), @@ -200,6 +202,7 @@ class HostedDemoConfig: return { "showDeviceControl": not self.hosted_demo, "showStripBackgroundControl": not self.hosted_demo, + "showQueuePanel": self.show_queue_panel, } def public_payload(self) -> dict[str, Any]: @@ -962,6 +965,7 @@ def create_app(): "stateUrl": f"/api/jobs/{job.job_id}", "queuePosition": state_payload["queuePosition"], "queueDepth": state_payload["queueDepth"], + "waitingCount": state_payload["waitingCount"], "queueCapacity": state_payload["queueCapacity"], "heartbeatIntervalSeconds": state_payload["heartbeatIntervalSeconds"], "staleAfterSeconds": state_payload["staleAfterSeconds"], diff --git a/src/repixelizer/gui_static/app.js b/src/repixelizer/gui_static/app.js index 9defe98..7dda6fc 100644 --- a/src/repixelizer/gui_static/app.js +++ b/src/repixelizer/gui_static/app.js @@ -45,6 +45,7 @@ const phaseYInput = byId("phaseYInput"); const stepsInput = byId("stepsInput"); const seedInput = byId("seedInput"); const runControlsCopy = byId("runControlsCopy"); +const queuePanel = byId("queuePanel"); const queueSummaryPanel = byId("queueSummary"); const cancelJobButton = byId("cancelJobButton"); const deviceField = byId("deviceField"); @@ -182,6 +183,11 @@ function renderEventLog() { } } function renderQueueSummary() { + if (!state.runtimeConfig?.ui.showQueuePanel) { + queueSummaryPanel.innerHTML = ""; + cancelJobButton.hidden = true; + return; + } queueSummaryPanel.innerHTML = ""; if (hasActiveJob()) { const cards = [ @@ -251,6 +257,15 @@ function setStage(stageKey, label, detail) { function hasActiveJob() { return state.jobId !== null && ["queued", "running", "waiting"].includes(state.status); } +function queueSummaryFromState(activeStatus = state.status) { + return { + queueDepth: state.queueDepth, + waitingCount: state.waitingCount, + queueCapacity: state.queueCapacity || (state.runtimeConfig?.limits.queueCapacity ?? 0), + hasActiveJob: state.queueDepth > 0, + activeStatus, + }; +} function formatByteLimit(bytes) { if (bytes >= 1024 * 1024) { return `${formatNumber(bytes / (1024 * 1024), 1)} MiB`; @@ -287,6 +302,7 @@ function applyRuntimeConfig(config) { state.runtimeConfig = config; deviceField.hidden = !config.ui.showDeviceControl; stripBackgroundField.hidden = !config.ui.showStripBackgroundControl; + queuePanel.hidden = !config.ui.showQueuePanel; stepsInput.max = String(config.limits.maxSteps); if (stepsInput.value.trim() === "" || Number(stepsInput.value) > config.limits.maxSteps || Number(stepsInput.value) === 48) { stepsInput.value = String(config.limits.defaultSteps); @@ -526,14 +542,19 @@ function renderStatusMetrics() { const items = []; const frame = getSelectedFrame(); if (state.stageKey === "queued" || state.status === "queued" || state.status === "waiting") { - items.push({ - label: "Line", - value: state.queuePosition === null || state.queueDepth <= 0 - ? "pending" - : `${state.queuePosition} / ${Math.max(state.queueDepth, state.queuePosition)}`, - }); - items.push({ label: "Waiting", value: `${state.waitingCount} / ${state.queueCapacity || (state.runtimeConfig?.limits.queueCapacity ?? 0)}` }); - items.push({ label: "Heartbeat", value: `${state.heartbeatIntervalSeconds}s` }); + if (state.runtimeConfig?.ui.showQueuePanel) { + items.push({ + label: "Line", + value: state.queuePosition === null || state.queueDepth <= 0 + ? "pending" + : `${state.queuePosition} / ${Math.max(state.queueDepth, state.queuePosition)}`, + }); + items.push({ + label: "Waiting", + value: `${state.waitingCount} / ${state.queueCapacity || (state.runtimeConfig?.limits.queueCapacity ?? 0)}`, + }); + items.push({ label: "Heartbeat", value: `${state.heartbeatIntervalSeconds}s` }); + } } else if (state.stageKey === "inference") { if (state.latticeSearch) { @@ -1120,6 +1141,11 @@ async function loadRuntimeConfig() { applyRuntimeConfig((await response.json())); } async function refreshQueueSummary() { + if (!state.runtimeConfig?.ui.showQueuePanel) { + state.queueSummary = null; + renderQueueSummary(); + return; + } if (hasActiveJob()) { return; } @@ -1137,6 +1163,10 @@ function stopQueuePolling() { } } function syncQueuePolling() { + if (!state.runtimeConfig?.ui.showQueuePanel) { + stopQueuePolling(); + return; + } if (hasActiveJob()) { stopQueuePolling(); return; @@ -1280,13 +1310,7 @@ async function handleEvent(eventName, payload) { break; case "queue_state": applyQueueState(payload); - state.queueSummary = { - queueDepth: state.queueDepth, - waitingCount: state.waitingCount, - queueCapacity: state.queueCapacity, - hasActiveJob: state.queueDepth > 0, - activeStatus: state.status, - }; + state.queueSummary = queueSummaryFromState(); break; case "job_canceled": stopHeartbeat(); @@ -1295,8 +1319,8 @@ async function handleEvent(eventName, payload) { addLog("Canceled", String(payload.message ?? "Canceled the job.")); runButton.disabled = false; currentEventSource?.close(); + await refreshQueueSummary().catch(() => undefined); syncQueuePolling(); - void refreshQueueSummary().catch(() => undefined); break; case "job_failed": stopHeartbeat(); @@ -1305,8 +1329,8 @@ async function handleEvent(eventName, payload) { addLog("Failure", String(payload.message ?? "The run failed.")); runButton.disabled = false; currentEventSource?.close(); + await refreshQueueSummary().catch(() => undefined); syncQueuePolling(); - void refreshQueueSummary().catch(() => undefined); break; case "stage_started": setJobState("running"); @@ -1482,8 +1506,8 @@ async function handleEvent(eventName, payload) { runButton.disabled = false; currentEventSource?.close(); await loadEditorAsset(state.finalOutputImage); + await refreshQueueSummary().catch(() => undefined); syncQueuePolling(); - void refreshQueueSummary().catch(() => undefined); break; default: break; @@ -1537,13 +1561,7 @@ async function startRun() { const payload = (await response.json()); state.jobId = payload.jobId; applyJobSnapshot(payload); - state.queueSummary = { - queueDepth: payload.queueDepth, - waitingCount: payload.waitingCount, - queueCapacity: payload.queueCapacity, - hasActiveJob: payload.queueDepth > 0, - activeStatus: payload.status, - }; + state.queueSummary = queueSummaryFromState(payload.status); connectEventStream(payload.jobId); syncHeartbeat(); syncQueuePolling(); @@ -1784,11 +1802,13 @@ async function initializeApp() { catch (error) { addLog("Config", error instanceof Error ? error.message : "Failed to load runtime config."); } - try { - await refreshQueueSummary(); - } - catch (error) { - addLog("Queue", error instanceof Error ? error.message : "Failed to load queue summary."); + if (state.runtimeConfig?.ui.showQueuePanel) { + try { + await refreshQueueSummary(); + } + catch (error) { + addLog("Queue", error instanceof Error ? error.message : "Failed to load queue summary."); + } } syncQueuePolling(); await renderEverything(); diff --git a/src/repixelizer/gui_static/index.html b/src/repixelizer/gui_static/index.html index 769969a..749c687 100644 --- a/src/repixelizer/gui_static/index.html +++ b/src/repixelizer/gui_static/index.html @@ -322,7 +322,7 @@ -