# Fix: Handle Navigation Away During Video Generation ## Problem When a user submits a video generation job and navigates away from the page: 1. Client-side polling stops 2. RunPod job continues but results are never fetched 3. Content stays stuck as "processing" forever 4. Video file never gets saved to disk ## Solution: Two-Part Fix ### Part 1: Background Job Processor (Server-Side) Create a background worker that periodically checks for stuck "processing" jobs and completes them. **New file: `src/services/jobProcessor.ts`** ```typescript // Runs every 30 seconds // Queries: SELECT * FROM generated_content WHERE status = 'processing' AND runpod_job_id IS NOT NULL // For each job: // 1. Poll RunPod status // 2. If COMPLETED: download file, update status to 'completed' // 3. If FAILED: update status to 'failed' with error message // 4. If still running and created_at > 15 minutes ago: mark as 'failed' (timeout) ``` **Modify: `src/index.ts`** - Import and start the job processor on server startup - Clean shutdown handling ### Part 2: Resume Polling on Page Load (Client-Side) When user returns to the app, check for their in-progress jobs and resume polling. **Modify: `public/js/app.js`** ```javascript // On login/page load: // 1. Call GET /api/content?status=processing to find pending jobs // 2. For each processing job with a runpod_job_id: // - Show notification "You have X jobs in progress" // - Optionally auto-resume polling for most recent one // 3. Update gallery to show real-time status ``` **New API endpoint: `GET /api/generate/pending`** - Returns user's jobs that are still processing - Include runpod_job_id so client can poll ## Files to Modify 1. **`src/services/jobProcessor.ts`** (NEW) - `startJobProcessor()` - starts interval - `stopJobProcessor()` - cleanup - `processStuckJobs()` - main logic 2. **`src/index.ts`** - Import jobProcessor - Call `startJobProcessor()` after DB init - Call `stopJobProcessor()` in shutdown handler 3. **`src/routes/generate.ts`** - Add `GET /pending` endpoint for user's processing jobs 4. **`public/js/app.js`** - Add `checkPendingJobs()` function - Call it after successful login in `showMainApp()` - Show UI notification for pending jobs - Add "Resume" button or auto-resume latest ## Implementation Details ### jobProcessor.ts ```typescript import { getDb } from '../db/index.js'; import { getJobStatus } from './runpodService.js'; import { updateContentStatus, saveContentFile } from './contentService.js'; import { logger } from '../utils/logger.js'; let processorInterval: NodeJS.Timeout | null = null; const POLL_INTERVAL = 30000; // 30 seconds const JOB_TIMEOUT = 15 * 60 * 1000; // 15 minutes export function startJobProcessor(): void { logger.info('Starting background job processor'); processorInterval = setInterval(processStuckJobs, POLL_INTERVAL); // Run immediately on startup processStuckJobs(); } export function stopJobProcessor(): void { if (processorInterval) { clearInterval(processorInterval); processorInterval = null; logger.info('Stopped background job processor'); } } async function processStuckJobs(): Promise { const db = getDb(); const pendingJobs = db.prepare(` SELECT * FROM generated_content WHERE status = 'processing' AND runpod_job_id IS NOT NULL `).all(); for (const job of pendingJobs) { try { const createdAt = new Date(job.created_at).getTime(); const age = Date.now() - createdAt; // Timeout check if (age > JOB_TIMEOUT) { updateContentStatus(job.id, 'failed', { errorMessage: 'Job timed out' }); continue; } // Poll RunPod const status = await getJobStatus(job.runpod_job_id); if (status.status === 'COMPLETED' && status.output?.outputs?.[0]) { const output = status.output.outputs[0]; if (output.data) { saveContentFile(job.id, output.data); } else { updateContentStatus(job.id, 'completed', { fileSize: output.size }); } logger.info({ contentId: job.id }, 'Background processor completed job'); } else if (status.status === 'FAILED') { updateContentStatus(job.id, 'failed', { errorMessage: status.error || 'Job failed' }); } } catch (error) { logger.error({ error, contentId: job.id }, 'Error processing stuck job'); } } } ``` ### Frontend changes (app.js) Add after `showMainApp()` is called: ```javascript 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) { // Create a notification banner const banner = document.createElement('div'); banner.className = 'pending-jobs-banner'; banner.innerHTML = ` You have ${jobs.length} video(s) generating `; document.querySelector('.main-content').prepend(banner); } ``` ### New endpoint in generate.ts ```typescript // Get user's pending jobs router.get('/pending', (req, res) => { const authReq = req as AuthenticatedRequest; const db = getDb(); const jobs = db.prepare(` SELECT id, runpod_job_id, prompt, created_at FROM generated_content WHERE user_id = ? AND status = 'processing' AND runpod_job_id IS NOT NULL ORDER BY created_at DESC `).all(authReq.user!.id); res.json({ jobs }); }); ``` ## CSS Addition (style.css) ```css .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; } .pending-jobs-banner button { 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; } ``` ## Testing 1. Start a generation job 2. Navigate to Gallery while processing 3. Verify background processor picks it up within 30 seconds 4. Verify job completes and file is saved 5. Test timeout scenario (mock a stuck job) 6. Test page reload shows pending jobs notification