Refine queue page polling
This commit is contained in:
+99
-55
@@ -326,7 +326,7 @@ QUEUE_HTML = r"""<!doctype html>
|
||||
<h2>What you can do here</h2>
|
||||
<p class="muted">Search stays focused on finding shows and adding new jobs. Queue now handles status, logs, retries, and cleanup as a separate workflow.</p>
|
||||
</div>
|
||||
<span class="badge">Auto refresh every 2.5s</span>
|
||||
<span class="badge">Active jobs update every 2.5s</span>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
@@ -351,7 +351,8 @@ QUEUE_HTML = r"""<!doctype html>
|
||||
queueTotal: 0,
|
||||
queueRequestId: 0,
|
||||
queueTimer: null,
|
||||
collapsedJobs: {}
|
||||
collapsedJobs: {},
|
||||
jobCards: {}
|
||||
};
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
@@ -382,11 +383,87 @@ QUEUE_HTML = r"""<!doctype html>
|
||||
state.collapsedJobs[jobId] = collapsed;
|
||||
}
|
||||
|
||||
function isTerminalStatus(status) {
|
||||
return ["done", "failed", "canceled"].includes(status);
|
||||
}
|
||||
|
||||
function buildJobSummary(job) {
|
||||
const libraryName = job.anime_name || job.title;
|
||||
const season = String(job.season || "1").padStart(2, "0");
|
||||
const seasonFolder = `Season ${season}`;
|
||||
const target = job.target_dir || `${job.download_dir}/${libraryName}/${seasonFolder}`;
|
||||
return `${libraryName} · S${season} · ${job.mode} · ${job.quality} · episodes ${job.episodes} · ${target}`;
|
||||
}
|
||||
|
||||
function buildJobLog(job) {
|
||||
return [...(job.log || [])].slice(-28).reverse().join("\n");
|
||||
}
|
||||
|
||||
function renderJobActions(job, actions) {
|
||||
actions.innerHTML = "";
|
||||
if (job.status === "running" || job.status === "pending") {
|
||||
actions.append(actionButton("Cancel", () => queueAction(job.id, "cancel"), "danger"));
|
||||
return;
|
||||
}
|
||||
actions.append(actionButton("Retry", () => queueAction(job.id, "retry")));
|
||||
actions.append(actionButton("Remove", () => queueAction(job.id, "remove")));
|
||||
}
|
||||
|
||||
function updateJobCard(item, job) {
|
||||
item.dataset.jobId = job.id;
|
||||
item.dataset.jobStatus = job.status;
|
||||
item.querySelector(".job-title").textContent = job.title;
|
||||
item.querySelector(".job-head .muted").textContent = buildJobSummary(job);
|
||||
const status = item.querySelector(".status");
|
||||
status.textContent = job.status;
|
||||
status.className = "status";
|
||||
status.classList.add(job.status);
|
||||
item.querySelector("pre").textContent = buildJobLog(job) || "Waiting...";
|
||||
item.querySelector(".toolbar .muted").textContent = job.exit_code === null ? "" : `exit ${job.exit_code}`;
|
||||
renderJobActions(job, item.querySelector(".row"));
|
||||
state.jobCards[job.id] = item;
|
||||
}
|
||||
|
||||
function createJobCard(job) {
|
||||
const item = document.createElement("article");
|
||||
item.className = "job";
|
||||
const collapsed = isCollapsed(job);
|
||||
if (collapsed) item.classList.add("collapsed");
|
||||
item.innerHTML = `
|
||||
<div class="job-head">
|
||||
<button class="job-toggle ghost" type="button" aria-label="Toggle job details" aria-expanded="true"></button>
|
||||
<div>
|
||||
<div class="job-title"></div>
|
||||
<p class="muted"></p>
|
||||
</div>
|
||||
<span class="status"></span>
|
||||
</div>
|
||||
<div class="job-body">
|
||||
<pre></pre>
|
||||
<div class="toolbar">
|
||||
<span class="muted"></span>
|
||||
<div class="row" style="flex:0 0 auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const toggle = item.querySelector(".job-toggle");
|
||||
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
toggle.addEventListener("click", () => {
|
||||
const nextCollapsed = !item.classList.contains("collapsed");
|
||||
item.classList.toggle("collapsed", nextCollapsed);
|
||||
toggle.setAttribute("aria-expanded", nextCollapsed ? "false" : "true");
|
||||
setCollapsed(job.id, nextCollapsed);
|
||||
});
|
||||
updateJobCard(item, job);
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderQueue(data) {
|
||||
const jobs = data.jobs || [];
|
||||
state.queuePage = data.page || 1;
|
||||
state.queuePages = data.pages || 1;
|
||||
state.queueTotal = data.total || 0;
|
||||
state.jobCards = {};
|
||||
const el = $("queue");
|
||||
el.innerHTML = "";
|
||||
if (!jobs.length) {
|
||||
@@ -395,58 +472,7 @@ QUEUE_HTML = r"""<!doctype html>
|
||||
return;
|
||||
}
|
||||
for (const job of jobs) {
|
||||
const item = document.createElement("article");
|
||||
item.className = "job";
|
||||
const collapsed = isCollapsed(job);
|
||||
if (collapsed) item.classList.add("collapsed");
|
||||
const log = [...(job.log || [])].slice(-28).reverse().join("\n");
|
||||
item.innerHTML = `
|
||||
<div class="job-head">
|
||||
<button class="job-toggle ghost" type="button" aria-label="Toggle job details" aria-expanded="true"></button>
|
||||
<div>
|
||||
<div class="job-title"></div>
|
||||
<p class="muted"></p>
|
||||
</div>
|
||||
<span class="status"></span>
|
||||
</div>
|
||||
<div class="job-body">
|
||||
<pre></pre>
|
||||
<div class="toolbar">
|
||||
<span class="muted"></span>
|
||||
<div class="row" style="flex:0 0 auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const toggle = item.querySelector(".job-toggle");
|
||||
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
toggle.addEventListener("click", () => {
|
||||
const nextCollapsed = !item.classList.contains("collapsed");
|
||||
item.classList.toggle("collapsed", nextCollapsed);
|
||||
toggle.setAttribute("aria-expanded", nextCollapsed ? "false" : "true");
|
||||
setCollapsed(job.id, nextCollapsed);
|
||||
});
|
||||
item.querySelector(".job-title").textContent = job.title;
|
||||
const libraryName = job.anime_name || job.title;
|
||||
const season = String(job.season || "1").padStart(2, "0");
|
||||
const seasonFolder = `Season ${season}`;
|
||||
const target = job.target_dir || `${job.download_dir}/${libraryName}/${seasonFolder}`;
|
||||
item.querySelector(".job-head .muted").textContent =
|
||||
`${libraryName} · S${season} · ${job.mode} · ${job.quality} · episodes ${job.episodes} · ${target}`;
|
||||
const status = item.querySelector(".status");
|
||||
status.textContent = job.status;
|
||||
status.classList.add(job.status);
|
||||
item.querySelector("pre").textContent = log || "Waiting...";
|
||||
item.querySelector(".toolbar .muted").textContent = job.exit_code === null ? "" : `exit ${job.exit_code}`;
|
||||
const actions = item.querySelector(".row");
|
||||
if (job.status === "running") {
|
||||
actions.append(actionButton("Cancel", () => queueAction(job.id, "cancel"), "danger"));
|
||||
} else if (job.status === "pending") {
|
||||
actions.append(actionButton("Cancel", () => queueAction(job.id, "cancel"), "danger"));
|
||||
} else {
|
||||
actions.append(actionButton("Retry", () => queueAction(job.id, "retry")));
|
||||
actions.append(actionButton("Remove", () => queueAction(job.id, "remove")));
|
||||
}
|
||||
el.appendChild(item);
|
||||
el.appendChild(createJobCard(job));
|
||||
}
|
||||
renderQueuePager();
|
||||
}
|
||||
@@ -501,13 +527,31 @@ QUEUE_HTML = r"""<!doctype html>
|
||||
return data;
|
||||
}
|
||||
|
||||
async function refreshVisibleJob(jobId) {
|
||||
const item = state.jobCards[jobId];
|
||||
if (!item) return null;
|
||||
const job = await api(`/api/queue/${jobId}`);
|
||||
if (!state.jobCards[jobId]) return job;
|
||||
updateJobCard(state.jobCards[jobId], job);
|
||||
return job;
|
||||
}
|
||||
|
||||
async function pollVisibleJobs() {
|
||||
const activeJobIds = Object.entries(state.jobCards)
|
||||
.filter(([, item]) => item && !isTerminalStatus(item.dataset.jobStatus || ""))
|
||||
.map(([jobId]) => jobId);
|
||||
for (const jobId of activeJobIds) {
|
||||
await refreshVisibleJob(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
$("removeFinishedBtn").addEventListener("click", () => queueBulkAction("/api/queue/clear-finished", "Removed finished jobs"));
|
||||
$("retryFailedBtn").addEventListener("click", () => queueBulkAction("/api/queue/retry-failed", "Retried failed jobs"));
|
||||
|
||||
(async function init() {
|
||||
try {
|
||||
await loadQueue();
|
||||
state.queueTimer = startSerialPoll(() => loadQueue(), 2500);
|
||||
state.queueTimer = startSerialPoll(() => pollVisibleJobs(), 2500);
|
||||
} catch (error) {
|
||||
setNotice(error.message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user