info( 'Daily generation skipped: already running (lock held).', 'scheduler' ); return; } try { ( new PRAutoBlogger_Pipeline_Runner() )->run(); // Release lock only if no articles were queued for chained processing. if ( ! get_option( 'prautoblogger_article_queue' ) ) { PRAutoBlogger_Generation_Lock::release(); } } catch ( \Throwable $e ) { PRAutoBlogger_Logger::instance()->error( sprintf( 'Daily generation %s: %s', get_class( $e ), $e->getMessage() ), 'scheduler' ); $this->mark_current_run_failed(); PRAutoBlogger_Generation_Lock::release(); } } /** * Handle the metrics collection cron event. * Side effects: API calls to GA4, database writes to ab_content_scores. */ public function on_collect_metrics(): void { try { ( new PRAutoBlogger_Metrics_Collector() )->collect_all(); } catch ( \Throwable $e ) { PRAutoBlogger_Logger::instance()->error( sprintf( 'Metrics collection %s: %s', get_class( $e ), $e->getMessage() ), 'metrics' ); } } /** * AJAX handler: kick off manual generation as a background cron job. * Delegates to PRAutoBlogger_Generation_Status_Poller. * * Side effects: schedules a WP-Cron event, writes a status transient. */ public function on_ajax_generate_now(): void { $this->poller()->on_ajax_generate_now(); } /** * Cron handler: runs pipeline orchestration + first article. * * If the pipeline queues additional articles (2..N), they will fire as * chained cron events — each in its own PHP process. The lock is released * by the pipeline when the last article completes, not here. */ public function on_manual_generation(): void { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @ignore_user_abort( true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @set_time_limit( 300 ); if ( ! PRAutoBlogger_Generation_Lock::acquire() ) { set_transient( PRAutoBlogger_Generation_Status_Poller::STATUS_TRANSIENT, array( 'status' => 'error', 'message' => __( 'Could not acquire generation lock. Another run may be in progress.', 'prautoblogger' ), ), PRAutoBlogger_Generation_Status_Poller::STATUS_TTL ); return; } try { $this->poller()->update_generation_stage( __( 'Collecting sources from Reddit...', 'prautoblogger' ) ); $runner = ( new PRAutoBlogger_Pipeline_Runner() )->set_skip_dedup( true ); $results = $runner->run(); // If no articles were queued (target=1 or 0 ideas), finalize here. // When articles ARE queued, the pipeline handles status + lock release. if ( ! get_option( 'prautoblogger_article_queue' ) ) { set_transient( PRAutoBlogger_Generation_Status_Poller::STATUS_TRANSIENT, array( 'status' => 'complete', 'generated' => $results['generated'], 'published' => $results['published'], 'rejected' => $results['rejected'], 'cost' => $results['cost'], ), PRAutoBlogger_Generation_Status_Poller::STATUS_TTL ); PRAutoBlogger_Generation_Lock::release(); } } catch ( \Throwable $e ) { PRAutoBlogger_Logger::instance()->error( sprintf( 'Manual generation %s: %s', get_class( $e ), $e->getMessage() ), 'pipeline' ); set_transient( PRAutoBlogger_Generation_Status_Poller::STATUS_TRANSIENT, array( 'status' => 'error', 'message' => $e->getMessage(), ), PRAutoBlogger_Generation_Status_Poller::STATUS_TTL ); $this->mark_current_run_failed(); PRAutoBlogger_Generation_Lock::release(); } } /** Cron handler: process next queued article. Chained by Pipeline_Runner. */ public function on_process_article_queue(): void { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @ignore_user_abort( true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @set_time_limit( 300 ); try { ( new PRAutoBlogger_Pipeline_Runner() )->process_next_queued_article(); } catch ( \Throwable $e ) { PRAutoBlogger_Logger::instance()->error( sprintf( 'Queued article generation %s: %s', get_class( $e ), $e->getMessage() ), 'pipeline' ); // Clean up on catastrophic failure. delete_option( 'prautoblogger_article_queue' ); PRAutoBlogger_Generation_Lock::release(); set_transient( PRAutoBlogger_Generation_Status_Poller::STATUS_TRANSIENT, array( 'status' => 'error', 'message' => $e->getMessage(), ), PRAutoBlogger_Generation_Status_Poller::STATUS_TTL ); } } /** AJAX: return generation status for frontend polling. Delegates to poller. */ public function on_ajax_generation_status(): void { $this->poller()->on_ajax_generation_status(); } /** * Lazy-load the generation status poller. * * @return PRAutoBlogger_Generation_Status_Poller */ private function poller(): PRAutoBlogger_Generation_Status_Poller { if ( null === $this->poller ) { $this->poller = new PRAutoBlogger_Generation_Status_Poller(); } return $this->poller; } /** * Mark this process's run failed after a fatal pipeline error * (no-op outside a run; a governor-halted run stays halted — final * states are sticky). * * @return void */ private function mark_current_run_failed(): void { $run_id = PRAutoBlogger_Run_Context::current_run_id(); if ( null !== $run_id ) { PRAutoBlogger_Run_State::mark_status( $run_id, 'failed' ); } } /** * Lazy-load the OpenRouter model registry singleton. * Config injected here — registry class has no knowledge of PRAUTOBLOGGER_* constants. * * @return PRAutoBlogger_OpenRouter_Model_Registry */ public function get_model_registry(): PRAutoBlogger_OpenRouter_Model_Registry { if ( null === $this->model_registry ) { $this->model_registry = new PRAutoBlogger_OpenRouter_Model_Registry( 'prautoblogger_openrouter_model_registry', 'prautoblogger_openrouter_model_registry_cache' ); } return $this->model_registry; } }