Make queue UI optional and fix idle summary
This commit is contained in:
parent
76b3936ecd
commit
7206b454db
7 changed files with 165 additions and 64 deletions
|
|
@ -56,6 +56,7 @@ type RuntimeConfig = {
|
|||
ui: {
|
||||
showDeviceControl: boolean;
|
||||
showStripBackgroundControl: boolean;
|
||||
showQueuePanel: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -213,6 +214,7 @@ const phaseYInput = byId<HTMLInputElement>("phaseYInput");
|
|||
const stepsInput = byId<HTMLInputElement>("stepsInput");
|
||||
const seedInput = byId<HTMLInputElement>("seedInput");
|
||||
const runControlsCopy = byId<HTMLParagraphElement>("runControlsCopy");
|
||||
const queuePanel = byId<HTMLElement>("queuePanel");
|
||||
const queueSummaryPanel = byId<HTMLDivElement>("queueSummary");
|
||||
const cancelJobButton = byId<HTMLButtonElement>("cancelJobButton");
|
||||
const deviceField = byId<HTMLElement>("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<void> {
|
|||
}
|
||||
|
||||
async function refreshQueueSummary(): Promise<void> {
|
||||
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<void
|
|||
break;
|
||||
case "queue_state":
|
||||
applyQueueState(payload as Partial<JobStateSnapshot>);
|
||||
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<void
|
|||
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();
|
||||
|
|
@ -1580,8 +1606,8 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise<void
|
|||
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");
|
||||
|
|
@ -1795,8 +1821,8 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise<void
|
|||
runButton.disabled = false;
|
||||
currentEventSource?.close();
|
||||
await loadEditorAsset(state.finalOutputImage);
|
||||
await refreshQueueSummary().catch(() => undefined);
|
||||
syncQueuePolling();
|
||||
void refreshQueueSummary().catch(() => undefined);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -1852,13 +1878,7 @@ async function startRun(): Promise<void> {
|
|||
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<void> {
|
|||
} 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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@
|
|||
<button id="runButton" class="button" type="button">Run The Machine</button>
|
||||
</section>
|
||||
|
||||
<section class="card sidebar-section ui-surface ui-frame">
|
||||
<section id="queuePanel" class="card sidebar-section ui-surface ui-frame" hidden>
|
||||
<div class="card-head">
|
||||
<div class="copy-stack">
|
||||
<h2>Queue</h2>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import importlib.util
|
||||
import io
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
|
@ -172,6 +173,25 @@ def test_repo_gui_runner_dispatches_to_gui_main(monkeypatch, capsys) -> None:
|
|||
assert "Reload: on" in output
|
||||
|
||||
|
||||
def test_repo_gui_runner_queue_ui_override_sets_env(monkeypatch) -> None:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
script_path = repo_root / "scripts" / "run_gui.py"
|
||||
spec = importlib.util.spec_from_file_location("repixelizer_run_gui_script", script_path)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
monkeypatch.delenv("REPIXELIZER_SHOW_QUEUE_PANEL", raising=False)
|
||||
monkeypatch.setattr(module, "_reclaim_stale_gui_port", lambda host, port, repo_root: None)
|
||||
monkeypatch.setattr("repixelizer.gui.main", lambda **_: 0)
|
||||
|
||||
exit_code = module.main(["--queue-ui", "show"])
|
||||
|
||||
assert exit_code == 0
|
||||
assert os.environ["REPIXELIZER_SHOW_QUEUE_PANEL"] == "1"
|
||||
|
||||
|
||||
def test_repo_gui_runner_reclaims_stale_gui_port_before_launch(monkeypatch, capsys) -> None:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
script_path = repo_root / "scripts" / "run_gui.py"
|
||||
|
|
@ -441,6 +461,25 @@ def test_gui_hosted_config_endpoint_exposes_demo_limits(monkeypatch, tmp_path: P
|
|||
assert payload["limits"]["defaultSteps"] == 32
|
||||
assert payload["ui"]["showDeviceControl"] is False
|
||||
assert payload["ui"]["showStripBackgroundControl"] is False
|
||||
assert payload["ui"]["showQueuePanel"] is True
|
||||
|
||||
|
||||
def test_gui_queue_panel_defaults_off_for_local_runs_and_can_be_forced(monkeypatch, tmp_path: Path) -> None:
|
||||
from repixelizer.gui import create_app
|
||||
|
||||
monkeypatch.delenv("REPIXELIZER_HOSTED_DEMO", raising=False)
|
||||
monkeypatch.delenv("REPIXELIZER_SHOW_QUEUE_PANEL", raising=False)
|
||||
monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool"))
|
||||
|
||||
app = create_app()
|
||||
payload = _response_json(_route_endpoint(app, "/api/config", "GET")())
|
||||
assert payload["hostedDemo"] is False
|
||||
assert payload["ui"]["showQueuePanel"] is False
|
||||
|
||||
monkeypatch.setenv("REPIXELIZER_SHOW_QUEUE_PANEL", "1")
|
||||
app = create_app()
|
||||
payload = _response_json(_route_endpoint(app, "/api/config", "GET")())
|
||||
assert payload["ui"]["showQueuePanel"] is True
|
||||
|
||||
|
||||
def test_gui_queue_rejects_eleventh_waiting_job(monkeypatch, tmp_path: Path) -> None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue