Add background job queue system for generation
- Implements sequential job queue with background worker thread (_enqueue_job, _queue_worker) - All generate routes now return job_id instead of prompt_id; frontend polls /api/queue/<id>/status - Queue management UI in navbar with live badge, job list, pause/resume/remove controls - Fix: replaced url_for() calls inside finalize callbacks with direct string paths (url_for raises RuntimeError without request context in background threads) - Batch cover generation now uses two-phase pattern: queue all jobs upfront, then poll concurrently via Promise.all so page navigation doesn't interrupt the process - Strengths gallery sweep migrated to same two-phase pattern; sgStop() cancels queued jobs server-side - LoRA weight randomisation via lora_weight_min/lora_weight_max already present in _resolve_lora_weight Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,12 @@
|
||||
<a href="/gallery" class="btn btn-sm btn-outline-light">Gallery</a>
|
||||
<a href="/settings" class="btn btn-sm btn-outline-light">Settings</a>
|
||||
<div class="vr mx-1 d-none d-lg-block"></div>
|
||||
<!-- Queue indicator -->
|
||||
<button id="queue-btn" class="btn btn-sm queue-btn" data-bs-toggle="modal" data-bs-target="#queueModal" title="Generation Queue">
|
||||
<span class="queue-icon">⏳</span>
|
||||
<span id="queue-count-badge" class="queue-badge d-none">0</span>
|
||||
</button>
|
||||
<div class="vr mx-1 d-none d-lg-block"></div>
|
||||
<!-- Service status indicators -->
|
||||
<span id="status-comfyui" class="service-status" title="ComfyUI" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="ComfyUI: checking…">
|
||||
<span class="status-dot status-checking"></span>
|
||||
@@ -94,6 +100,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generation Queue Modal -->
|
||||
<div class="modal fade" id="queueModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
Generation Queue
|
||||
<span id="queue-modal-count" class="badge bg-secondary ms-2">0</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div id="queue-empty-msg" class="text-center text-muted py-4">
|
||||
<p class="mb-0">No jobs in queue.</p>
|
||||
</div>
|
||||
<ul id="queue-job-list" class="list-group list-group-flush d-none"></ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<small class="text-muted me-auto">Jobs are processed sequentially. Close this window to continue browsing.</small>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -339,6 +370,148 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// ---- Generation Queue UI ----
|
||||
(function() {
|
||||
const badge = document.getElementById('queue-count-badge');
|
||||
const modalCount = document.getElementById('queue-modal-count');
|
||||
const jobList = document.getElementById('queue-job-list');
|
||||
const emptyMsg = document.getElementById('queue-empty-msg');
|
||||
|
||||
const STATUS_LABELS = {
|
||||
pending: { text: 'Pending', cls: 'text-muted' },
|
||||
processing: { text: 'Generating…', cls: 'text-warning' },
|
||||
paused: { text: 'Paused', cls: 'text-secondary' },
|
||||
done: { text: 'Done', cls: 'text-success' },
|
||||
failed: { text: 'Failed', cls: 'text-danger' },
|
||||
removed: { text: 'Removed', cls: 'text-muted' },
|
||||
};
|
||||
|
||||
function renderQueue(jobs) {
|
||||
const activeJobs = jobs.filter(j => !['done', 'failed', 'removed'].includes(j.status));
|
||||
const count = activeJobs.length;
|
||||
|
||||
// Update badge
|
||||
if (count > 0) {
|
||||
badge.textContent = count;
|
||||
badge.classList.remove('d-none');
|
||||
document.getElementById('queue-btn').classList.add('queue-btn-active');
|
||||
} else {
|
||||
badge.classList.add('d-none');
|
||||
document.getElementById('queue-btn').classList.remove('queue-btn-active');
|
||||
}
|
||||
|
||||
// Update modal count
|
||||
if (modalCount) modalCount.textContent = jobs.length;
|
||||
|
||||
// Render job list
|
||||
if (!jobList) return;
|
||||
if (jobs.length === 0) {
|
||||
jobList.classList.add('d-none');
|
||||
if (emptyMsg) emptyMsg.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
jobList.classList.remove('d-none');
|
||||
if (emptyMsg) emptyMsg.classList.add('d-none');
|
||||
|
||||
jobList.innerHTML = '';
|
||||
jobs.forEach(job => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item d-flex align-items-center gap-2 py-2';
|
||||
li.id = `queue-job-${job.id}`;
|
||||
|
||||
const statusInfo = STATUS_LABELS[job.status] || { text: job.status, cls: 'text-muted' };
|
||||
|
||||
// Status indicator
|
||||
const statusDot = document.createElement('span');
|
||||
statusDot.className = `queue-status-dot queue-status-${job.status}`;
|
||||
li.appendChild(statusDot);
|
||||
|
||||
// Label
|
||||
const label = document.createElement('span');
|
||||
label.className = 'flex-grow-1 small';
|
||||
label.textContent = job.label;
|
||||
li.appendChild(label);
|
||||
|
||||
// Status text
|
||||
const statusText = document.createElement('span');
|
||||
statusText.className = `small ${statusInfo.cls}`;
|
||||
statusText.textContent = statusInfo.text;
|
||||
if (job.status === 'failed' && job.error) {
|
||||
statusText.title = job.error;
|
||||
statusText.style.cursor = 'help';
|
||||
}
|
||||
li.appendChild(statusText);
|
||||
|
||||
// Action buttons
|
||||
const btnGroup = document.createElement('div');
|
||||
btnGroup.className = 'd-flex gap-1';
|
||||
|
||||
if (job.status === 'pending') {
|
||||
const pauseBtn = document.createElement('button');
|
||||
pauseBtn.className = 'btn btn-xs btn-outline-secondary';
|
||||
pauseBtn.textContent = '⏸';
|
||||
pauseBtn.title = 'Pause';
|
||||
pauseBtn.onclick = () => queuePause(job.id);
|
||||
btnGroup.appendChild(pauseBtn);
|
||||
}
|
||||
if (job.status === 'paused') {
|
||||
const resumeBtn = document.createElement('button');
|
||||
resumeBtn.className = 'btn btn-xs btn-outline-success';
|
||||
resumeBtn.textContent = '▶';
|
||||
resumeBtn.title = 'Resume';
|
||||
resumeBtn.onclick = () => queuePause(job.id);
|
||||
btnGroup.appendChild(resumeBtn);
|
||||
}
|
||||
if (['pending', 'paused', 'failed'].includes(job.status)) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'btn btn-xs btn-outline-danger';
|
||||
removeBtn.textContent = '✕';
|
||||
removeBtn.title = 'Remove';
|
||||
removeBtn.onclick = () => queueRemove(job.id);
|
||||
btnGroup.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
li.appendChild(btnGroup);
|
||||
jobList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchQueue() {
|
||||
try {
|
||||
const resp = await fetch('/api/queue');
|
||||
const data = await resp.json();
|
||||
renderQueue(data.jobs || []);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function queueRemove(jobId) {
|
||||
try {
|
||||
await fetch(`/api/queue/${jobId}/remove`, { method: 'POST' });
|
||||
fetchQueue();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function queuePause(jobId) {
|
||||
try {
|
||||
await fetch(`/api/queue/${jobId}/pause`, { method: 'POST' });
|
||||
fetchQueue();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Poll queue every 2 seconds
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchQueue();
|
||||
setInterval(fetchQueue, 2000);
|
||||
|
||||
// Refresh when modal opens
|
||||
const queueModal = document.getElementById('queueModal');
|
||||
if (queueModal) {
|
||||
queueModal.addEventListener('show.bs.modal', fetchQueue);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user