queueManager = $queueManager ?? new ContaiQueueManager(); } public function enqueuePostGeneration(int $count, array $config): array { if ($count < 1) { throw new InvalidArgumentException('Number of posts must be at least 1'); } $batchId = $this->generateBatchId(); $config['batch_id'] = $batchId; $enqueuedCount = $this->queueManager->enqueuePostGeneration($count, $config); // Persist BOTH the originally requested count and what was actually // enqueued. `enqueuedCount < requested` happens when there are fewer // pending keywords than the wizard asked for — surfaces as a hard // failure in getBatchStatus() / SiteGenerationJob::waitForPosts() // instead of silently marking the batch "complete" with a short load. update_option("contai_batch_{$batchId}_requested", $count); update_option("contai_batch_{$batchId}_total", $enqueuedCount); update_option("contai_batch_{$batchId}_started_at", current_time('mysql')); contai_log(sprintf( '[site-gen] batch %s enqueued: requested=%d enqueued=%d', $batchId, $count, $enqueuedCount )); return [ 'success' => true, 'batch_id' => $batchId, 'requested_count' => $count, 'enqueued_count' => $enqueuedCount, ]; } public function getBatchStatus(string $batchId): array { global $wpdb; $total = (int) get_option("contai_batch_{$batchId}_total", 0); // Falls back to $total for batches that started before the // requested-count tracking was added. Pre-existing batches behave as // before (no shortfall surfaced) instead of being retroactively failed. $requested = (int) get_option("contai_batch_{$batchId}_requested", $total); $table = $wpdb->prefix . 'contai_jobs'; $likePattern = '%"batch_id":"' . $wpdb->esc_like($batchId) . '"%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table is from $wpdb->prefix, safe. $done = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT COUNT(*) FROM {$table} WHERE job_type = 'post_generation' AND status = 'done' AND payload LIKE %s", $likePattern )); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $failed = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT COUNT(*) FROM {$table} WHERE job_type = 'post_generation' AND status = 'failed' AND payload LIKE %s", $likePattern )); $finished = $done + $failed; return [ 'batch_id' => $batchId, 'requested' => $requested, 'total' => $total, 'completed' => $done, 'failed' => $failed, 'finished' => $finished, 'is_complete' => $finished >= $total, // True when the wizard asked for more posts than could be // enqueued (e.g. fewer keywords available than num_posts). Used // by SiteGenerationJob to fail loudly instead of marking the // generation done with a short load. 'is_short' => $requested > $total, 'shortfall' => max(0, $requested - $total), ]; } private function generateBatchId(): string { return 'batch_' . uniqid() . '_' . time(); } }