Fix stuck processing jobs and increase timeouts
Background Job Processor: - Add src/services/jobProcessor.ts that polls RunPod every 30s for stuck jobs - Automatically completes or fails jobs that were abandoned (user navigated away) - Times out jobs after 25 minutes Client-Side Resume: - Add GET /api/generate/pending endpoint to fetch user's processing jobs - Add checkPendingJobs() that runs on login/page load - Show notification banner when user has jobs generating in background - Add "View Progress" button to resume polling for a job Timeout Increases (10min → 25min): - src/utils/validators.ts: request validation max/default - src/config.ts: RUNPOD_MAX_TIMEOUT_MS default - public/js/app.js: client-side polling maxTime - src/services/jobProcessor.ts: background processor timeout CI/CD Optimization: - Add paths-ignore to backend build.yaml to skip rebuilds on frontend-only changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -602,6 +602,45 @@ body {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Pending Jobs Banner */
|
||||
.pending-jobs-banner {
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.pending-jobs-banner span {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pending-jobs-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pending-jobs-banner .btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.pending-jobs-banner .btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.section-grid {
|
||||
@@ -621,4 +660,9 @@ body {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pending-jobs-banner {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,62 @@ function showMainApp() {
|
||||
}
|
||||
|
||||
showSection('generate');
|
||||
checkPendingJobs();
|
||||
}
|
||||
|
||||
// Pending Jobs
|
||||
async function checkPendingJobs() {
|
||||
try {
|
||||
const data = await api('/generate/pending');
|
||||
if (data.jobs && data.jobs.length > 0) {
|
||||
showPendingJobsNotification(data.jobs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check pending jobs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showPendingJobsNotification(jobs) {
|
||||
// Remove existing banner if any
|
||||
const existingBanner = document.querySelector('.pending-jobs-banner');
|
||||
if (existingBanner) existingBanner.remove();
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'pending-jobs-banner';
|
||||
banner.innerHTML = `
|
||||
<span>You have ${jobs.length} video${jobs.length > 1 ? 's' : ''} generating in the background</span>
|
||||
<div class="pending-jobs-actions">
|
||||
<button class="btn btn-sm" onclick="resumeLatestJob(${jobs[0].id}, '${jobs[0].runpod_job_id}')">View Progress</button>
|
||||
<button class="btn btn-sm" onclick="this.closest('.pending-jobs-banner').remove()">Dismiss</button>
|
||||
</div>
|
||||
`;
|
||||
document.querySelector('.main-content').prepend(banner);
|
||||
}
|
||||
|
||||
async function resumeLatestJob(contentId, jobId) {
|
||||
// Switch to generate tab and show progress
|
||||
showSection('generate');
|
||||
|
||||
const statusEl = document.getElementById('generation-status');
|
||||
const videoEl = document.getElementById('output-video');
|
||||
const btn = document.getElementById('generate-btn');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generating...';
|
||||
statusEl.className = 'status-message info';
|
||||
statusEl.textContent = 'Resuming job...';
|
||||
statusEl.classList.remove('hidden');
|
||||
videoEl.classList.add('hidden');
|
||||
|
||||
// Remove the banner
|
||||
const banner = document.querySelector('.pending-jobs-banner');
|
||||
if (banner) banner.remove();
|
||||
|
||||
// Poll for completion
|
||||
await pollJob(jobId, contentId, statusEl, videoEl);
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Generate Video';
|
||||
}
|
||||
|
||||
// Login
|
||||
@@ -268,7 +324,7 @@ document.getElementById('generate-form').addEventListener('submit', async (e) =>
|
||||
|
||||
async function pollJob(jobId, contentId, statusEl, videoEl) {
|
||||
const startTime = Date.now();
|
||||
const maxTime = 10 * 60 * 1000; // 10 minutes
|
||||
const maxTime = 25 * 60 * 1000; // 25 minutes
|
||||
|
||||
while (Date.now() - startTime < maxTime) {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
|
||||
Reference in New Issue
Block a user