feat: implement job resumption, custom queue reordering, and auto-venv start script setup

This commit is contained in:
2026-06-24 13:37:17 -04:00
parent 2ceb948b98
commit 212291ea94
6 changed files with 569 additions and 140 deletions
+101 -6
View File
@@ -1276,6 +1276,37 @@ async function loadGallery() {
}
}
async function moveQueueItem(jobId, direction) {
try {
const res = await fetch("/api/queue");
if (!res.ok) return;
const data = await res.json();
const queue = data.queue;
const index = queue.indexOf(jobId);
if (index === -1) return;
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= queue.length) return;
// Swap
const temp = queue[index];
queue[index] = queue[newIndex];
queue[newIndex] = temp;
const reorderRes = await fetch("/api/queue/reorder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ job_ids: queue })
});
if (reorderRes.ok) {
loadQueue();
}
} catch (err) {
console.error("Error reordering queue:", err);
}
}
async function loadQueue() {
const queueList = document.getElementById("queue-list");
if (!queueList) return;
@@ -1287,7 +1318,7 @@ async function loadQueue() {
const jobs = await res.json();
const displayJobs = jobs.filter(job =>
["queued", "analyzing", "extracting", "upscaling", "assembling", "completed", "failed", "cancelled"].includes(job.status)
["queued", "analyzing", "extracting", "upscaling", "assembling", "completed", "failed", "cancelled", "interrupted"].includes(job.status)
);
if (displayJobs.length === 0) {
@@ -1318,13 +1349,40 @@ async function loadQueue() {
if (job.status === "failed" && job.error) {
metaHtml += `<span style="color: var(--danger);"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${job.error}</span>`;
} else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled" && job.status !== "completed") {
} else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled" && job.status !== "completed" && job.status !== "interrupted") {
metaHtml += `<span><i class="fa-solid fa-clock"></i> ETA: ${job.eta || 'Calculating...'}</span>`;
}
let actionsHtml = "";
if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) {
let reorderHtml = "";
if (job.status === "queued" && job.queue_position !== null && job.queue_position !== undefined) {
const queuedJobs = displayJobs.filter(j => j.status === "queued");
const isFirst = job.queue_position === 0;
const isLast = job.queue_position === queuedJobs.length - 1;
reorderHtml = `
<button class="btn btn-secondary btn-small btn-move-up" data-id="${job.job_id}" style="padding: 0.25rem 0.5rem; width: 30px; height: 30px; border-radius: 8px; font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; ${isFirst ? 'visibility: hidden;' : ''}" title="Move Up">
<i class="fa-solid fa-arrow-up"></i>
</button>
<button class="btn btn-secondary btn-small btn-move-down" data-id="${job.job_id}" style="padding: 0.25rem 0.5rem; width: 30px; height: 30px; border-radius: 8px; font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; ${isLast ? 'visibility: hidden;' : ''}" title="Move Down">
<i class="fa-solid fa-arrow-down"></i>
</button>
`;
}
if (job.status === "interrupted") {
actionsHtml = `
<button class="btn btn-primary btn-small btn-resume-job" data-id="${job.job_id}">
<i class="fa-solid fa-play"></i> Resume
</button>
<button class="btn btn-secondary btn-small btn-delete-job" data-id="${job.job_id}">
<i class="fa-solid fa-trash"></i> Clear
</button>
`;
} else if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) {
actionsHtml = `
${reorderHtml}
<button class="btn btn-primary btn-small btn-view-job" data-id="${job.job_id}">
<i class="fa-solid fa-eye"></i> View
</button>
@@ -1366,9 +1424,44 @@ async function loadQueue() {
</div>
`;
const upBtn = item.querySelector(".btn-move-up");
if (upBtn) {
upBtn.addEventListener("click", async (e) => {
e.stopPropagation();
await moveQueueItem(job.job_id, -1);
});
}
const downBtn = item.querySelector(".btn-move-down");
if (downBtn) {
downBtn.addEventListener("click", async (e) => {
e.stopPropagation();
await moveQueueItem(job.job_id, 1);
});
}
const resumeBtn = item.querySelector(".btn-resume-job");
if (resumeBtn) {
resumeBtn.addEventListener("click", async (e) => {
e.stopPropagation();
try {
const resumeRes = await fetch(`/api/upscale/resume/${job.job_id}`, { method: "POST" });
if (resumeRes.ok) {
loadQueue();
} else {
const err = await resumeRes.json();
alert("Failed to resume job: " + formatFetchError(err, "unknown error"));
}
} catch (err) {
console.error("Error resuming job:", err);
}
});
}
const abortBtn = item.querySelector(".btn-abort-job");
if (abortBtn) {
abortBtn.addEventListener("click", async () => {
abortBtn.addEventListener("click", async (e) => {
e.stopPropagation();
if (confirm("Are you sure you want to abort this upscaling job?")) {
try {
const abortRes = await fetch(`/api/upscale/cancel/${job.job_id}`, { method: "POST" });
@@ -1384,7 +1477,8 @@ async function loadQueue() {
const deleteBtn = item.querySelector(".btn-delete-job");
if (deleteBtn) {
deleteBtn.addEventListener("click", async () => {
deleteBtn.addEventListener("click", async (e) => {
e.stopPropagation();
if (confirm("Are you sure you want to clear this job from history?")) {
try {
const delRes = await fetch(`/api/jobs/${job.job_id}`, { method: "DELETE" });
@@ -1401,7 +1495,8 @@ async function loadQueue() {
const viewBtn = item.querySelector(".btn-view-job");
if (viewBtn) {
viewBtn.addEventListener("click", () => {
viewBtn.addEventListener("click", (e) => {
e.stopPropagation();
currentJobId = job.job_id;
if (socket) {
try {