fix: move Generate Now to background processing to avoid 120s timeout
Hostinger's LiteSpeed web server kills any HTTP connection that runs longer than 120 seconds with a 500 "Request Timeout" error. The full pipeline (Reddit fetch → topic scoring → article generation → editorial pass → image gen ×2) takes 120-180s, causing "Request failed: error" on every manual generation attempt. Architecture change: - Generate Now AJAX handler now schedules a one-shot WP-Cron event and returns immediately (~100ms response time) - New prautoblogger_manual_generation cron action runs the pipeline in a separate PHP process free from the connection timeout - Pipeline progress is written to a transient at each stage - New prautoblogger_generation_status AJAX endpoint lets the frontend poll progress every 5 seconds - Frontend auto-resumes polling on page load if a generation is running - Direct wp-cron.php loopback ensures cron fires even if DISABLE_WP_CRON is set (common on Hostinger) Version bump: 0.2.9 → 0.3.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,25 @@
|
|||||||
$referer.val(refUrl.pathname + refUrl.search);
|
$referer.val(refUrl.pathname + refUrl.search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resume polling if a generation is already running (e.g., user
|
||||||
|
// navigated away and came back, or opened a new tab mid-generation).
|
||||||
|
var $genBtn = $('#prautoblogger-generate-now');
|
||||||
|
if ($genBtn.length && config.ajaxUrl) {
|
||||||
|
$.ajax({
|
||||||
|
url: config.ajaxUrl,
|
||||||
|
method: 'POST',
|
||||||
|
data: { action: 'prautoblogger_generation_status', nonce: config.generateNonce },
|
||||||
|
timeout: 10000,
|
||||||
|
success: function (response) {
|
||||||
|
if (response.success && response.data && response.data.status === 'running') {
|
||||||
|
setButtonLoading($genBtn, config.generatingText || 'Generating…');
|
||||||
|
showProgress(response.data.stage || 'Generation in progress…');
|
||||||
|
pollGenerationStatus($genBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -146,29 +165,76 @@
|
|||||||
* Shows pipeline stages while the request is in-flight so the user
|
* Shows pipeline stages while the request is in-flight so the user
|
||||||
* knows it hasn't hung.
|
* knows it hasn't hung.
|
||||||
*/
|
*/
|
||||||
var generateStages = [
|
/*
|
||||||
{ text: 'Collecting sources from Reddit…', delay: 0 },
|
* Pipeline stages are now reported by the server via the status polling
|
||||||
{ text: 'Analyzing topics and scoring…', delay: 8000 },
|
* endpoint. The old client-side fake timers have been removed.
|
||||||
{ text: 'Selecting best topic…', delay: 16000 },
|
*/
|
||||||
{ text: 'Generating article draft via AI…', delay: 24000 },
|
|
||||||
{ text: 'Running editorial pass…', delay: 50000 },
|
/** Poll interval handle for generation status checks. */
|
||||||
{ text: 'Saving and publishing…', delay: 75000 },
|
var statusPollTimer = null;
|
||||||
{ text: 'Almost done — finalizing…', delay: 100000 }
|
|
||||||
];
|
/**
|
||||||
var stageTimers = [];
|
* Poll the generation status endpoint until the run finishes.
|
||||||
|
* Updates the progress UI and resolves the button state on completion.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $btn The Generate Now button.
|
||||||
|
*/
|
||||||
|
function pollGenerationStatus($btn) {
|
||||||
|
statusPollTimer = setInterval(function () {
|
||||||
|
$.ajax({
|
||||||
|
url: config.ajaxUrl,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'prautoblogger_generation_status',
|
||||||
|
nonce: config.generateNonce
|
||||||
|
},
|
||||||
|
timeout: 15000,
|
||||||
|
success: function (response) {
|
||||||
|
if (!response.success) return;
|
||||||
|
var d = response.data;
|
||||||
|
|
||||||
|
if (d.status === 'running') {
|
||||||
|
showProgress(d.stage || 'Generating…');
|
||||||
|
return; // Keep polling.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal state — stop polling.
|
||||||
|
clearInterval(statusPollTimer);
|
||||||
|
statusPollTimer = null;
|
||||||
|
hideProgress();
|
||||||
|
resetButton($btn, config.generateText || 'Generate Now');
|
||||||
|
|
||||||
|
if (d.status === 'complete') {
|
||||||
|
showStatus(
|
||||||
|
'Generation complete: ' + d.generated + ' generated, ' +
|
||||||
|
d.published + ' published, ' + d.rejected + ' rejected. ' +
|
||||||
|
'Cost: $' + parseFloat(d.cost).toFixed(4),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
} else if (d.status === 'error') {
|
||||||
|
showStatus(
|
||||||
|
'Generation failed: ' + (d.message || 'Unknown error'),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// idle or unknown — generation may have finished before
|
||||||
|
// polling started. Reset the button cleanly.
|
||||||
|
hideProgress();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
// Network blip — keep polling, don't bail on transient errors.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000); // Poll every 5 seconds.
|
||||||
|
}
|
||||||
|
|
||||||
$(document).on('click', '#prautoblogger-generate-now', function () {
|
$(document).on('click', '#prautoblogger-generate-now', function () {
|
||||||
var $btn = $(this);
|
var $btn = $(this);
|
||||||
if ($btn.prop('disabled')) return;
|
if ($btn.prop('disabled')) return;
|
||||||
|
|
||||||
setButtonLoading($btn, config.generatingText || 'Generating…');
|
setButtonLoading($btn, config.generatingText || 'Generating…');
|
||||||
|
showProgress('Starting generation…');
|
||||||
// Start pipeline stage messages.
|
|
||||||
stageTimers = [];
|
|
||||||
$.each(generateStages, function (i, stage) {
|
|
||||||
var t = setTimeout(function () { showProgress(stage.text); }, stage.delay);
|
|
||||||
stageTimers.push(t);
|
|
||||||
});
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: config.ajaxUrl,
|
url: config.ajaxUrl,
|
||||||
@@ -178,21 +244,19 @@
|
|||||||
nonce: config.generateNonce,
|
nonce: config.generateNonce,
|
||||||
force: '1' // Always clear stale locks on manual runs.
|
force: '1' // Always clear stale locks on manual runs.
|
||||||
},
|
},
|
||||||
timeout: 300000, // 5 minutes — generation can be slow.
|
timeout: 30000, // 30s is plenty — this just schedules the cron.
|
||||||
success: function (response) {
|
success: function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
var d = response.data;
|
// Generation kicked off in background — start polling.
|
||||||
showStatus(
|
showProgress(response.data.message || 'Generation started…');
|
||||||
'Generation complete: ' + d.generated + ' generated, ' +
|
pollGenerationStatus($btn);
|
||||||
d.published + ' published, ' + d.rejected + ' rejected. ' +
|
|
||||||
'Cost: $' + parseFloat(d.cost).toFixed(4),
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
showStatus(
|
showStatus(
|
||||||
'Generation failed: ' + (response.data && response.data.message || 'Unknown error'),
|
'Generation failed: ' + (response.data && response.data.message || 'Unknown error'),
|
||||||
'error'
|
'error'
|
||||||
);
|
);
|
||||||
|
hideProgress();
|
||||||
|
resetButton($btn, config.generateText || 'Generate Now');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function (xhr, status) {
|
error: function (xhr, status) {
|
||||||
@@ -200,11 +264,6 @@
|
|||||||
'Request failed: ' + status + '. Check server error logs.',
|
'Request failed: ' + status + '. Check server error logs.',
|
||||||
'error'
|
'error'
|
||||||
);
|
);
|
||||||
},
|
|
||||||
complete: function () {
|
|
||||||
// Clear all stage timers.
|
|
||||||
$.each(stageTimers, function (_, t) { clearTimeout(t); });
|
|
||||||
stageTimers = [];
|
|
||||||
hideProgress();
|
hideProgress();
|
||||||
resetButton($btn, config.generateText || 'Generate Now');
|
resetButton($btn, config.generateText || 'Generate Now');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,11 +59,20 @@ class PRAutoBlogger_Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Transient key for background generation status. */
|
||||||
|
private const STATUS_TRANSIENT = 'prautoblogger_generation_status';
|
||||||
|
|
||||||
|
/** How long to keep the result available for polling (seconds). */
|
||||||
|
private const STATUS_TTL = 600;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler: trigger manual generation.
|
* AJAX handler: kick off manual generation as a background cron job.
|
||||||
* Verifies nonce/capability, then runs the pipeline with the same DB lock.
|
|
||||||
*
|
*
|
||||||
* Side effects: API calls, database writes, WordPress post creation.
|
* Returns immediately so the browser never hits Hostinger's 120-second
|
||||||
|
* connection timeout. The frontend polls on_ajax_generation_status()
|
||||||
|
* every few seconds to get progress and final results.
|
||||||
|
*
|
||||||
|
* Side effects: schedules a WP-Cron event, writes a status transient.
|
||||||
*/
|
*/
|
||||||
public function on_ajax_generate_now(): void {
|
public function on_ajax_generate_now(): void {
|
||||||
check_ajax_referer( 'prautoblogger_generate_now', 'nonce' );
|
check_ajax_referer( 'prautoblogger_generate_now', 'nonce' );
|
||||||
@@ -73,37 +82,160 @@ class PRAutoBlogger_Executor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LLM calls take 30-60+ seconds; extend PHP limit (may be blocked by host).
|
|
||||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
|
||||||
@set_time_limit( 300 );
|
|
||||||
|
|
||||||
$force = isset( $_POST['force'] ) && '1' === $_POST['force'];
|
$force = isset( $_POST['force'] ) && '1' === $_POST['force'];
|
||||||
if ( $force ) {
|
if ( $force ) {
|
||||||
$this->release_generation_lock();
|
$this->release_generation_lock();
|
||||||
|
delete_transient( self::STATUS_TRANSIENT );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a run is already in progress.
|
||||||
|
$current = get_transient( self::STATUS_TRANSIENT );
|
||||||
|
if ( is_array( $current ) && 'running' === ( $current['status'] ?? '' ) ) {
|
||||||
|
wp_send_json_success( [
|
||||||
|
'background' => true,
|
||||||
|
'message' => __( 'Generation already in progress.', 'prautoblogger' ),
|
||||||
|
] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write initial "running" status for the frontend to poll.
|
||||||
|
set_transient( self::STATUS_TRANSIENT, [
|
||||||
|
'status' => 'running',
|
||||||
|
'stage' => __( 'Starting generation...', 'prautoblogger' ),
|
||||||
|
'started' => time(),
|
||||||
|
], self::STATUS_TTL );
|
||||||
|
|
||||||
|
// Schedule immediate one-shot cron event. WordPress will fire this
|
||||||
|
// on the next page load (or via wp-cron.php) in a separate PHP process
|
||||||
|
// that is not subject to the 120-second connection timeout.
|
||||||
|
if ( ! wp_next_scheduled( 'prautoblogger_manual_generation' ) ) {
|
||||||
|
wp_schedule_single_event( time(), 'prautoblogger_manual_generation' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn the cron immediately via a non-blocking loopback request
|
||||||
|
// so we don't depend on the next visitor to trigger it.
|
||||||
|
// Some hosts (including Hostinger) set DISABLE_WP_CRON = true and
|
||||||
|
// rely on an external cron runner. spawn_cron() respects that flag
|
||||||
|
// and may no-op, so we also fire a direct loopback as a fallback.
|
||||||
|
spawn_cron();
|
||||||
|
|
||||||
|
// Direct non-blocking loopback to wp-cron.php — ensures the event
|
||||||
|
// fires even if DISABLE_WP_CRON is true or spawn_cron() no-ops.
|
||||||
|
wp_remote_post(
|
||||||
|
site_url( 'wp-cron.php?doing_wp_cron=' . sprintf( '%.22F', microtime( true ) ) ),
|
||||||
|
[
|
||||||
|
'timeout' => 0.01,
|
||||||
|
'blocking' => false,
|
||||||
|
'sslverify' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_send_json_success( [
|
||||||
|
'background' => true,
|
||||||
|
'message' => __( 'Generation started in background. Polling for status...', 'prautoblogger' ),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cron handler for manual (background) generation.
|
||||||
|
*
|
||||||
|
* Runs in a separate PHP process triggered by WP-Cron, free from
|
||||||
|
* the web server's connection timeout. Writes progress and final
|
||||||
|
* results to a transient that the frontend polls.
|
||||||
|
*
|
||||||
|
* Side effects: API calls, database writes, WordPress post creation,
|
||||||
|
* transient updates.
|
||||||
|
*/
|
||||||
|
public function on_manual_generation(): void {
|
||||||
|
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||||
|
@set_time_limit( 300 );
|
||||||
|
|
||||||
if ( ! $this->acquire_generation_lock() ) {
|
if ( ! $this->acquire_generation_lock() ) {
|
||||||
wp_send_json_error( [ 'message' => __( 'A generation run is already in progress. Pass force=1 to clear the lock.', 'prautoblogger' ) ] );
|
set_transient( self::STATUS_TRANSIENT, [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => __( 'Could not acquire generation lock. Another run may be in progress.', 'prautoblogger' ),
|
||||||
|
], self::STATUS_TTL );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Manual runs skip dedup so "Generate Now" always produces content.
|
// Update stage for frontend polling.
|
||||||
|
$this->update_generation_stage( __( 'Collecting sources from Reddit...', 'prautoblogger' ) );
|
||||||
|
|
||||||
$results = ( new PRAutoBlogger_Pipeline_Runner() )
|
$results = ( new PRAutoBlogger_Pipeline_Runner() )
|
||||||
->set_skip_dedup( true )
|
->set_skip_dedup( true )
|
||||||
->run();
|
->run();
|
||||||
wp_send_json_success( $results );
|
|
||||||
|
set_transient( self::STATUS_TRANSIENT, [
|
||||||
|
'status' => 'complete',
|
||||||
|
'generated' => $results['generated'],
|
||||||
|
'published' => $results['published'],
|
||||||
|
'rejected' => $results['rejected'],
|
||||||
|
'cost' => $results['cost'],
|
||||||
|
], self::STATUS_TTL );
|
||||||
} catch ( \Throwable $e ) {
|
} catch ( \Throwable $e ) {
|
||||||
PRAutoBlogger_Logger::instance()->error(
|
PRAutoBlogger_Logger::instance()->error(
|
||||||
sprintf( 'Manual generation %s: %s', get_class( $e ), $e->getMessage() ),
|
sprintf( 'Manual generation %s: %s', get_class( $e ), $e->getMessage() ),
|
||||||
'pipeline'
|
'pipeline'
|
||||||
);
|
);
|
||||||
wp_send_json_error( [ 'message' => $e->getMessage() ] );
|
set_transient( self::STATUS_TRANSIENT, [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
], self::STATUS_TTL );
|
||||||
} finally {
|
} finally {
|
||||||
$this->release_generation_lock();
|
$this->release_generation_lock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler: return current generation status for frontend polling.
|
||||||
|
*
|
||||||
|
* Lightweight endpoint — reads a transient and returns it. Called every
|
||||||
|
* few seconds by admin.js while generation is in progress.
|
||||||
|
*
|
||||||
|
* Side effects: none.
|
||||||
|
*/
|
||||||
|
public function on_ajax_generation_status(): void {
|
||||||
|
check_ajax_referer( 'prautoblogger_generate_now', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = get_transient( self::STATUS_TRANSIENT );
|
||||||
|
|
||||||
|
if ( ! is_array( $status ) ) {
|
||||||
|
wp_send_json_success( [ 'status' => 'idle' ] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still "running" but started more than 10 minutes ago, assume it died.
|
||||||
|
if ( 'running' === $status['status'] && isset( $status['started'] ) && ( time() - $status['started'] ) > self::STATUS_TTL ) {
|
||||||
|
delete_transient( self::STATUS_TRANSIENT );
|
||||||
|
wp_send_json_success( [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => __( 'Generation timed out. Check the Activity Log for details.', 'prautoblogger' ),
|
||||||
|
] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( $status );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: update the generation stage for frontend polling.
|
||||||
|
*
|
||||||
|
* @param string $stage Human-readable stage description.
|
||||||
|
*/
|
||||||
|
private function update_generation_stage( string $stage ): void {
|
||||||
|
$current = get_transient( self::STATUS_TRANSIENT );
|
||||||
|
if ( is_array( $current ) ) {
|
||||||
|
$current['stage'] = $stage;
|
||||||
|
set_transient( self::STATUS_TRANSIENT, $current, self::STATUS_TTL );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler: generate images for existing posts that lack them.
|
* AJAX handler: generate images for existing posts that lack them.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ class PRAutoBlogger {
|
|||||||
add_action( 'prautoblogger_daily_generation', [ $this->executor, 'on_daily_generation' ] );
|
add_action( 'prautoblogger_daily_generation', [ $this->executor, 'on_daily_generation' ] );
|
||||||
add_action( 'prautoblogger_collect_metrics', [ $this->executor, 'on_collect_metrics' ] );
|
add_action( 'prautoblogger_collect_metrics', [ $this->executor, 'on_collect_metrics' ] );
|
||||||
|
|
||||||
|
// Manual "Generate Now" runs as a one-shot cron event to avoid
|
||||||
|
// Hostinger's 120-second web-server connection timeout.
|
||||||
|
add_action( 'prautoblogger_manual_generation', [ $this->executor, 'on_manual_generation' ] );
|
||||||
|
|
||||||
$registry = $this->executor->get_model_registry();
|
$registry = $this->executor->get_model_registry();
|
||||||
add_action( 'prautoblogger_refresh_model_registry', [ $registry, 'refresh' ] );
|
add_action( 'prautoblogger_refresh_model_registry', [ $registry, 'refresh' ] );
|
||||||
}
|
}
|
||||||
@@ -92,6 +96,7 @@ class PRAutoBlogger {
|
|||||||
/** Register AJAX handlers for admin actions. */
|
/** Register AJAX handlers for admin actions. */
|
||||||
private function register_ajax_hooks(): void {
|
private function register_ajax_hooks(): void {
|
||||||
add_action( 'wp_ajax_prautoblogger_generate_now', [ $this->executor, 'on_ajax_generate_now' ] );
|
add_action( 'wp_ajax_prautoblogger_generate_now', [ $this->executor, 'on_ajax_generate_now' ] );
|
||||||
|
add_action( 'wp_ajax_prautoblogger_generation_status', [ $this->executor, 'on_ajax_generation_status' ] );
|
||||||
add_action( 'wp_ajax_prautoblogger_generate_image', [ $this->executor, 'on_ajax_generate_image' ] );
|
add_action( 'wp_ajax_prautoblogger_generate_image', [ $this->executor, 'on_ajax_generate_image' ] );
|
||||||
add_action( 'wp_ajax_prautoblogger_test_connection', [ $this->executor, 'on_ajax_test_connection' ] );
|
add_action( 'wp_ajax_prautoblogger_test_connection', [ $this->executor, 'on_ajax_test_connection' ] );
|
||||||
add_action( 'wp_ajax_prautoblogger_get_models', [ $this->executor, 'on_ajax_get_models' ] );
|
add_action( 'wp_ajax_prautoblogger_get_models', [ $this->executor, 'on_ajax_get_models' ] );
|
||||||
|
|||||||
@@ -61,17 +61,20 @@ class PRAutoBlogger_Pipeline_Runner {
|
|||||||
$target_count = absint( get_option( 'prautoblogger_daily_article_target', 1 ) );
|
$target_count = absint( get_option( 'prautoblogger_daily_article_target', 1 ) );
|
||||||
|
|
||||||
// Step 1: Collect source data.
|
// Step 1: Collect source data.
|
||||||
|
$this->broadcast_stage( __( 'Collecting sources from Reddit…', 'prautoblogger' ) );
|
||||||
$collector = new PRAutoBlogger_Source_Collector();
|
$collector = new PRAutoBlogger_Source_Collector();
|
||||||
$collector->collect_from_all_sources();
|
$collector->collect_from_all_sources();
|
||||||
|
|
||||||
// Step 2: Analyze collected data for patterns.
|
// Step 2: Analyze collected data for patterns.
|
||||||
// Pass a generous target so the LLM returns enough diverse ideas for
|
// Pass a generous target so the LLM returns enough diverse ideas for
|
||||||
// scoring to pick from — ask for 2× the daily target so dedup has room.
|
// scoring to pick from — ask for 2× the daily target so dedup has room.
|
||||||
|
$this->broadcast_stage( __( 'Analyzing topics and scoring…', 'prautoblogger' ) );
|
||||||
$llm = new PRAutoBlogger_OpenRouter_Provider();
|
$llm = new PRAutoBlogger_OpenRouter_Provider();
|
||||||
$analyzer = new PRAutoBlogger_Content_Analyzer( $llm, $cost_tracker );
|
$analyzer = new PRAutoBlogger_Content_Analyzer( $llm, $cost_tracker );
|
||||||
$analysis = $analyzer->analyze_recent_data( max( $target_count * 2, 6 ) );
|
$analysis = $analyzer->analyze_recent_data( max( $target_count * 2, 6 ) );
|
||||||
|
|
||||||
// Step 3: Score and deduplicate ideas.
|
// Step 3: Score and deduplicate ideas.
|
||||||
|
$this->broadcast_stage( __( 'Selecting best topic…', 'prautoblogger' ) );
|
||||||
$scorer = new PRAutoBlogger_Idea_Scorer();
|
$scorer = new PRAutoBlogger_Idea_Scorer();
|
||||||
$scorer->set_skip_dedup( $this->skip_dedup );
|
$scorer->set_skip_dedup( $this->skip_dedup );
|
||||||
$ideas = $scorer->score_and_rank( $analysis, $target_count );
|
$ideas = $scorer->score_and_rank( $analysis, $target_count );
|
||||||
@@ -99,9 +102,11 @@ class PRAutoBlogger_Pipeline_Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$this->broadcast_stage( __( 'Generating article draft via AI…', 'prautoblogger' ) );
|
||||||
$content = $generator->generate( $idea );
|
$content = $generator->generate( $idea );
|
||||||
$generated++;
|
$generated++;
|
||||||
|
|
||||||
|
$this->broadcast_stage( __( 'Running editorial pass…', 'prautoblogger' ) );
|
||||||
$review = $editor->review( $content, $idea );
|
$review = $editor->review( $content, $idea );
|
||||||
|
|
||||||
if ( 'approved' === $review->get_verdict() || 'revised' === $review->get_verdict() ) {
|
if ( 'approved' === $review->get_verdict() || 'revised' === $review->get_verdict() ) {
|
||||||
@@ -112,6 +117,7 @@ class PRAutoBlogger_Pipeline_Runner {
|
|||||||
? ( $review->get_revised_content() ?? $content )
|
? ( $review->get_revised_content() ?? $content )
|
||||||
: $content;
|
: $content;
|
||||||
|
|
||||||
|
$this->broadcast_stage( __( 'Saving and publishing…', 'prautoblogger' ) );
|
||||||
if ( $auto_publish ) {
|
if ( $auto_publish ) {
|
||||||
$publisher->publish( $final_content, $idea, $review, $cost_tracker->get_run_id() );
|
$publisher->publish( $final_content, $idea, $review, $cost_tracker->get_run_id() );
|
||||||
$published++;
|
$published++;
|
||||||
@@ -144,4 +150,22 @@ class PRAutoBlogger_Pipeline_Runner {
|
|||||||
'cost' => $total_cost,
|
'cost' => $total_cost,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the generation status transient with the current pipeline stage.
|
||||||
|
*
|
||||||
|
* Called at key checkpoints during the pipeline so the frontend status
|
||||||
|
* poller can show real progress instead of fake timers. No-ops if the
|
||||||
|
* transient doesn't exist (e.g., during daily cron runs).
|
||||||
|
*
|
||||||
|
* @param string $stage Human-readable stage description.
|
||||||
|
*/
|
||||||
|
private function broadcast_stage( string $stage ): void {
|
||||||
|
$transient_key = 'prautoblogger_generation_status';
|
||||||
|
$current = get_transient( $transient_key );
|
||||||
|
if ( is_array( $current ) && 'running' === ( $current['status'] ?? '' ) ) {
|
||||||
|
$current['stage'] = $stage;
|
||||||
|
set_transient( $transient_key, $current, 600 );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ declare(strict_types=1);
|
|||||||
* Plugin Name: PRAutoBlogger
|
* Plugin Name: PRAutoBlogger
|
||||||
* Plugin URI: https://peptiderepo.com/prautoblogger
|
* Plugin URI: https://peptiderepo.com/prautoblogger
|
||||||
* Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics.
|
* Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics.
|
||||||
* Version: 0.2.9
|
* Version: 0.3.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
* Author: PeptideRepo
|
* Author: PeptideRepo
|
||||||
@@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
| and limits without magic strings.
|
| and limits without magic strings.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
define( 'PRAUTOBLOGGER_VERSION', '0.2.9' );
|
define( 'PRAUTOBLOGGER_VERSION', '0.3.0' );
|
||||||
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
|
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
|
||||||
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
||||||
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user