getMessage() . "\n"; exit(1); } // 2. Check pending queue items echo "\n--- Queue Status ---\n"; $stmt = $db->query("SELECT status, COUNT(*) as cnt FROM invoice_processing_queue GROUP BY status"); $rows = $stmt->fetchAll(); if (empty($rows)) { echo " (empty — no items in queue at all)\n"; } else { foreach ($rows as $r) { echo " {$r['status']}: {$r['cnt']}\n"; } } // 3. Check batch statuses echo "\n--- Batch Status ---\n"; $stmt = $db->query("SELECT status, COUNT(*) as cnt FROM invoice_batches GROUP BY status"); $rows = $stmt->fetchAll(); if (empty($rows)) { echo " (empty — no batches)\n"; } else { foreach ($rows as $r) { echo " {$r['status']}: {$r['cnt']}\n"; } } // 4. Check for stuck items (processing but no worker) echo "\n--- Stuck Items (processing for >5 minutes) ---\n"; $stmt = $db->query(" SELECT q.id, q.batch_id, q.status, q.image_path, q.created_at, q.error_message FROM invoice_processing_queue q WHERE q.status IN ('pending', 'processing') ORDER BY q.created_at DESC LIMIT 10 "); $stuck = $stmt->fetchAll(); if (empty($stuck)) { echo " (none — all clear)\n"; } else { foreach ($stuck as $s) { $exists = file_exists($s['image_path']) ? '✓ file exists' : '✗ FILE MISSING'; echo " ID={$s['id']} | Status={$s['status']} | $exists\n"; echo " Path: {$s['image_path']}\n"; if ($s['error_message']) echo " Error: {$s['error_message']}\n"; } } // 5. Check lock file echo "\n--- Lock File ---\n"; $lockFile = STORAGE_PATH . '/logs/process_batches.lock'; if (file_exists($lockFile)) { $age = time() - filemtime($lockFile); $content = trim(file_get_contents($lockFile)); echo " ⚠ Lock file EXISTS (age: {$age}s, content: $content)\n"; if ($age > 300) { echo " → This lock is STALE. Removing...\n"; @unlink($lockFile); echo " ✓ Removed.\n"; } } else { echo " ✓ No lock file (good)\n"; } // 6. Check key files echo "\n--- Key Files ---\n"; $files = [ 'InvoiceProcessor' => APP_PATH . '/Services/InvoiceProcessor.php', 'AI' => APP_PATH . '/Core/AI.php', 'process_batches' => APP_PATH . '/cron/process_batches.php', 'worker.log' => STORAGE_PATH . '/logs/worker.log', ]; foreach ($files as $name => $path) { if (file_exists($path)) { echo " ✓ $name: $path (" . filesize($path) . " bytes)\n"; } else { echo " ✗ $name: MISSING — $path\n"; } } // 7. Check Gemini API key echo "\n--- Configuration ---\n"; $apiKey = env('GEMINI_API_KEY'); echo " GEMINI_API_KEY: " . ($apiKey ? "✓ Set (" . strlen($apiKey) . " chars)" : "✗ MISSING!") . "\n"; echo " APP_DEBUG: " . env('APP_DEBUG', 'false') . "\n"; echo " fastcgi_finish_request: " . (function_exists('fastcgi_finish_request') ? '✓ Available' : '✗ Not available (CLI mode)') . "\n"; // 8. Show last lines of worker.log echo "\n--- Last 20 lines of worker.log ---\n"; $workerLog = STORAGE_PATH . '/logs/worker.log'; if (file_exists($workerLog)) { $lines = file($workerLog); $last = array_slice($lines, -20); foreach ($last as $line) { echo " " . rtrim($line) . "\n"; } } else { echo " (worker.log does not exist yet)\n"; } // 9. Try to reset any stuck 'processing' items back to 'pending' echo "\n--- Fix Stuck Items? ---\n"; $stmt = $db->query("SELECT COUNT(*) FROM invoice_processing_queue WHERE status = 'processing'"); $stuckCount = (int)$stmt->fetchColumn(); if ($stuckCount > 0) { echo " Found $stuckCount items stuck in 'processing' state.\n"; $db->query("UPDATE invoice_processing_queue SET status = 'pending' WHERE status = 'processing'"); echo " ✓ Reset them to 'pending' so they can be reprocessed.\n"; } else { echo " ✓ No stuck items.\n"; } echo "\n=== Diagnostics Complete ===\n"; echo "Next step: Run 'php app/cron/process_batches.php' to process pending items.\n";