feat: add AJAX batch enrichment (v4.4.0)
Replace the WP-Cron-based re-enrichment with an AJAX polling approach that processes one peptide per request. This bypasses WP-Cron entirely, which is unreliable on Hostinger shared hosting with LiteSpeed + Cloudflare. New files: - includes/class-psa-batch-enrichment.php — AJAX endpoints for status + process - assets/js/psa-batch-enrich.js — Admin progress bar UI with polling loop The admin settings page now shows a "Re-enrich Peptides" button with live progress bar, per-item log, and automatic retry on failure. Respects the daily generation cap and monthly budget. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,12 +27,14 @@ peptide-search-ai/
|
||||
│ ├── class-psa-cost-tracker.php API usage logging, monthly budget enforcement, cost estimation
|
||||
│ ├── class-psa-pubchem.php PubChem PUG REST API integration for molecular enrichment
|
||||
│ ├── class-psa-admin.php Settings page, usage dashboard, migration tools, re-enrichment actions
|
||||
│ ├── class-psa-batch-enrichment.php AJAX-based batch re-enrichment (processes one peptide per request, no WP-Cron)
|
||||
│ └── class-psa-template.php Single peptide page rendering; quick-facts, extended data, badges
|
||||
│
|
||||
├── assets/
|
||||
│ ├── js/
|
||||
│ │ ├── peptide-search.js Frontend search, AJAX handler, result rendering (jQuery)
|
||||
│ │ └── psa-directory.js Directory grid, category filters, detail modal (vanilla JS)
|
||||
│ │ ├── psa-directory.js Directory grid, category filters, detail modal (vanilla JS)
|
||||
│ │ └── psa-batch-enrich.js Admin batch enrichment UI (progress bar, AJAX polling loop)
|
||||
│ └── css/
|
||||
│ ├── peptide-search.css Scoped styles for search UI, results, pending states, badges
|
||||
│ └── psa-directory.css Directory card grid, filter pills, modal styles, dark mode support
|
||||
|
||||
172
assets/js/psa-batch-enrich.js
Executable file
172
assets/js/psa-batch-enrich.js
Executable file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Batch enrichment UI controller for the Peptide Search AI admin page.
|
||||
*
|
||||
* What: Drives the AJAX polling loop that enriches one peptide at a time with a progress bar.
|
||||
* Who loads it: PSA_Admin::enqueue_batch_scripts() on the PSA settings page.
|
||||
* Dependencies: jQuery (WP core), wp.ajax (WP admin AJAX helper), psaBatchEnrich localized data.
|
||||
*
|
||||
* @see includes/class-psa-batch-enrichment.php
|
||||
* @see includes/class-psa-admin.php
|
||||
*/
|
||||
/* global jQuery, psaBatchEnrich */
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
var isRunning = false;
|
||||
var processed = 0;
|
||||
var errors = 0;
|
||||
var total = 0;
|
||||
var remaining = 0;
|
||||
|
||||
/**
|
||||
* Initialize: bind the start button and fetch initial status.
|
||||
*/
|
||||
function init() {
|
||||
var $btn = $('#psa-batch-enrich-start');
|
||||
if (!$btn.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$btn.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (isRunning) {
|
||||
return;
|
||||
}
|
||||
startEnrichment();
|
||||
});
|
||||
|
||||
// Fetch initial count on page load.
|
||||
fetchStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current enrichment status (how many need enrichment).
|
||||
*/
|
||||
function fetchStatus() {
|
||||
$.post(psaBatchEnrich.ajaxUrl, {
|
||||
action: 'psa_batch_enrich_status',
|
||||
_nonce: psaBatchEnrich.nonce
|
||||
}, function (response) {
|
||||
if (response.success) {
|
||||
remaining = response.data.remaining;
|
||||
total = response.data.remaining; // Total to process this run.
|
||||
updateStatusText(response.data.remaining + ' peptides need enrichment');
|
||||
|
||||
if (response.data.remaining === 0) {
|
||||
$('#psa-batch-enrich-start').prop('disabled', true).text('All enriched');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the enrichment loop.
|
||||
*/
|
||||
function startEnrichment() {
|
||||
isRunning = true;
|
||||
processed = 0;
|
||||
errors = 0;
|
||||
|
||||
$('#psa-batch-enrich-start').prop('disabled', true).text('Running...');
|
||||
$('#psa-batch-enrich-progress').show();
|
||||
$('#psa-batch-enrich-log').empty().show();
|
||||
|
||||
updateProgress(0, total);
|
||||
processNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process one peptide, then loop.
|
||||
*/
|
||||
function processNext() {
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.post(psaBatchEnrich.ajaxUrl, {
|
||||
action: 'psa_batch_enrich_next',
|
||||
_nonce: psaBatchEnrich.nonce
|
||||
}, function (response) {
|
||||
if (!response.success) {
|
||||
appendLog('Server error: ' + (response.data || 'Unknown'), 'error');
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
var data = response.data;
|
||||
|
||||
if (data.status === 'done') {
|
||||
appendLog('All peptides enriched!', 'success');
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'cap_reached') {
|
||||
appendLog(data.message, 'warning');
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
processed++;
|
||||
remaining = data.remaining;
|
||||
|
||||
if (data.status === 'ok') {
|
||||
appendLog('Enriched: ' + data.name, 'success');
|
||||
} else if (data.status === 'error') {
|
||||
errors++;
|
||||
appendLog('Failed: ' + data.name + ' — ' + data.message, 'error');
|
||||
}
|
||||
|
||||
updateProgress(processed, processed + remaining);
|
||||
|
||||
// Brief delay to avoid hammering the server — 2 seconds between requests.
|
||||
setTimeout(processNext, 2000);
|
||||
|
||||
}).fail(function (xhr) {
|
||||
appendLog('Request failed (HTTP ' + xhr.status + '). Retrying in 10s...', 'warning');
|
||||
setTimeout(processNext, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the progress bar.
|
||||
*/
|
||||
function updateProgress(done, total) {
|
||||
var pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
||||
$('#psa-batch-enrich-bar').css('width', pct + '%');
|
||||
$('#psa-batch-enrich-pct').text(pct + '% (' + done + '/' + total + ')');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status text below the button.
|
||||
*/
|
||||
function updateStatusText(text) {
|
||||
$('#psa-batch-enrich-status').text(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a line to the log.
|
||||
*/
|
||||
function appendLog(message, type) {
|
||||
var color = type === 'error' ? '#dc3232' : type === 'warning' ? '#dba617' : '#46b450';
|
||||
var icon = type === 'error' ? '✗' : type === 'warning' ? '⚠' : '✓';
|
||||
$('#psa-batch-enrich-log').prepend(
|
||||
'<div style="color:' + color + ';margin:2px 0;font-size:13px;">' +
|
||||
icon + ' ' + $('<span>').text(message).html() +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* End the run.
|
||||
*/
|
||||
function finish() {
|
||||
isRunning = false;
|
||||
$('#psa-batch-enrich-start').prop('disabled', false).text('Re-enrich Peptides');
|
||||
|
||||
var summary = 'Done. ' + processed + ' processed, ' + errors + ' errors, ' + remaining + ' remaining.';
|
||||
updateStatusText(summary);
|
||||
}
|
||||
|
||||
$(document).ready(init);
|
||||
})(jQuery);
|
||||
@@ -23,6 +23,7 @@ class PSA_Admin {
|
||||
add_action( 'admin_menu', array( __CLASS__, 'add_menu_pages' ) );
|
||||
add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
|
||||
add_action( 'admin_init', array( __CLASS__, 'handle_admin_actions' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_batch_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,6 +299,34 @@ class PSA_Admin {
|
||||
return $queued;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue batch enrichment script on the PSA settings page only.
|
||||
*
|
||||
* @param string $hook The admin page hook suffix.
|
||||
*/
|
||||
public static function enqueue_batch_scripts( string $hook ): void {
|
||||
if ( 'settings_page_peptide-search-ai' !== $hook ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'psa-batch-enrich',
|
||||
PSA_PLUGIN_URL . 'assets/js/psa-batch-enrich.js',
|
||||
array( 'jquery' ),
|
||||
PSA_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'psa-batch-enrich',
|
||||
'psaBatchEnrich',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'psa_batch_enrich' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Field renderers
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -386,7 +415,6 @@ class PSA_Admin {
|
||||
|
||||
public static function render_settings_page(): void {
|
||||
$migrate_url = wp_nonce_url( admin_url( 'options-general.php?page=peptide-search-ai&psa_action=migrate_categories' ), 'psa_migrate_categories' );
|
||||
$reenrich_url = wp_nonce_url( admin_url( 'options-general.php?page=peptide-search-ai&psa_action=reenrich' ), 'psa_reenrich' );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'Peptide Search AI Settings', 'peptide-search-ai' ); ?></h1>
|
||||
@@ -418,10 +446,19 @@ class PSA_Admin {
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Re-enrich Data', 'peptide-search-ai' ); ?></th>
|
||||
<td>
|
||||
<a href="<?php echo esc_url( $reenrich_url ); ?>" class="button" onclick="return confirm('<?php esc_attr_e( 'This will queue background AI regeneration for peptides missing extended data. It will consume API credits. Continue?', 'peptide-search-ai' ); ?>');">
|
||||
<?php esc_html_e( 'Re-enrich Existing Peptides', 'peptide-search-ai' ); ?>
|
||||
</a>
|
||||
<p class="description"><?php esc_html_e( 'Queues background regeneration for published peptides missing half-life, stability, and other extended fields. Respects daily generation cap.', 'peptide-search-ai' ); ?></p>
|
||||
<button type="button" id="psa-batch-enrich-start" class="button button-primary">
|
||||
<?php esc_html_e( 'Re-enrich Peptides', 'peptide-search-ai' ); ?>
|
||||
</button>
|
||||
<span id="psa-batch-enrich-status" style="margin-left:10px;color:#666;"></span>
|
||||
<p class="description"><?php esc_html_e( 'Processes one peptide at a time via AI — no WP-Cron dependency. Respects daily generation cap.', 'peptide-search-ai' ); ?></p>
|
||||
|
||||
<div id="psa-batch-enrich-progress" style="display:none;margin-top:10px;max-width:500px;">
|
||||
<div style="background:#e0e0e0;border-radius:3px;height:20px;overflow:hidden;">
|
||||
<div id="psa-batch-enrich-bar" style="background:#0073aa;height:100%;width:0%;transition:width 0.3s;"></div>
|
||||
</div>
|
||||
<span id="psa-batch-enrich-pct" style="font-size:12px;color:#666;"></span>
|
||||
</div>
|
||||
<div id="psa-batch-enrich-log" style="display:none;margin-top:10px;max-height:200px;overflow-y:auto;padding:8px;background:#f9f9f9;border:1px solid #ddd;border-radius:3px;max-width:500px;font-family:monospace;font-size:12px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
198
includes/class-psa-batch-enrichment.php
Executable file
198
includes/class-psa-batch-enrichment.php
Executable file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
/**
|
||||
* AJAX-based batch enrichment for peptides missing extended meta fields.
|
||||
*
|
||||
* What: Provides a cron-independent way to re-enrich peptides one at a time via AJAX polling.
|
||||
* Who calls it: PSA_Admin settings page JavaScript; AJAX hooks registered on admin_init.
|
||||
* Dependencies: PSA_AI_Generator (content generation), PSA_Config (caps), PSA_Cost_Tracker (budget).
|
||||
*
|
||||
* @package PeptideSearchAI
|
||||
* @see includes/class-psa-admin.php
|
||||
* @see includes/class-psa-ai-generator.php
|
||||
*/
|
||||
declare( strict_types=1 );
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class PSA_Batch_Enrichment {
|
||||
|
||||
/**
|
||||
* Register AJAX handlers for batch enrichment.
|
||||
* Called from psa_init() in the main plugin file.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action( 'wp_ajax_psa_batch_enrich_status', array( __CLASS__, 'ajax_status' ) );
|
||||
add_action( 'wp_ajax_psa_batch_enrich_next', array( __CLASS__, 'ajax_process_next' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Return the count of peptides that need enrichment.
|
||||
*
|
||||
* Response JSON: { total: int, remaining: int, completed: int }
|
||||
* Side effects: None (read-only).
|
||||
*/
|
||||
public static function ajax_status(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( 'Unauthorized', 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'psa_batch_enrich', '_nonce' );
|
||||
|
||||
$needs_enrichment = self::get_unenriched_ids();
|
||||
$total_published = self::get_total_published_count();
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'total' => $total_published,
|
||||
'remaining' => count( $needs_enrichment ),
|
||||
'completed' => $total_published - count( $needs_enrichment ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Process one peptide — set to draft, generate, republish.
|
||||
*
|
||||
* Response JSON on success: { post_id: int, name: string, status: 'ok', remaining: int }
|
||||
* Response JSON on skip: { status: 'done', remaining: 0 }
|
||||
* Response JSON on error: { post_id: int, name: string, status: 'error', message: string, remaining: int }
|
||||
*
|
||||
* Side effects: Updates post meta via AI generation, logs API cost.
|
||||
*/
|
||||
public static function ajax_process_next(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( 'Unauthorized', 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'psa_batch_enrich', '_nonce' );
|
||||
|
||||
// Respect daily generation cap.
|
||||
$daily_key = 'psa_daily_gen_' . gmdate( 'Y-m-d' );
|
||||
$daily_count = (int) get_transient( $daily_key );
|
||||
if ( $daily_count >= PSA_Config::DAILY_GENERATION_CAP ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'status' => 'cap_reached',
|
||||
'message' => sprintf(
|
||||
'Daily generation cap reached (%d/%d). Try again tomorrow.',
|
||||
$daily_count,
|
||||
PSA_Config::DAILY_GENERATION_CAP
|
||||
),
|
||||
'remaining' => count( self::get_unenriched_ids() ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$ids = self::get_unenriched_ids();
|
||||
if ( empty( $ids ) ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'status' => 'done',
|
||||
'remaining' => 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$post_id = $ids[0];
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
wp_send_json_error( 'Post not found: ' . $post_id, 404 );
|
||||
}
|
||||
|
||||
$peptide_name = $post->post_title;
|
||||
$original_status = $post->post_status;
|
||||
|
||||
// Temporarily set to draft + pending so background_generate processes it.
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_status' => 'draft',
|
||||
)
|
||||
);
|
||||
update_post_meta( $post_id, 'psa_source', 'pending' );
|
||||
update_post_meta( $post_id, 'psa_generation_started', time() );
|
||||
|
||||
// Call the generator directly (synchronous — no cron dependency).
|
||||
PSA_AI_Generator::background_generate( $post_id, $peptide_name );
|
||||
|
||||
// Bump daily counter.
|
||||
$daily_count = (int) get_transient( $daily_key );
|
||||
set_transient( $daily_key, $daily_count + 1, DAY_IN_SECONDS );
|
||||
|
||||
// Check outcome: did generation succeed?
|
||||
$error = get_post_meta( $post_id, 'psa_generation_error', true );
|
||||
$refreshed_post = get_post( $post_id );
|
||||
|
||||
// If still in draft (generation failed or auto_publish is 'draft'), force republish.
|
||||
if ( $refreshed_post && 'draft' === $refreshed_post->post_status ) {
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$remaining = count( self::get_unenriched_ids() );
|
||||
|
||||
if ( ! empty( $error ) ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'status' => 'error',
|
||||
'post_id' => $post_id,
|
||||
'name' => $peptide_name,
|
||||
'message' => $error,
|
||||
'remaining' => $remaining,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'status' => 'ok',
|
||||
'post_id' => $post_id,
|
||||
'name' => $peptide_name,
|
||||
'remaining' => $remaining,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IDs of published peptides missing the half_life extended field.
|
||||
*
|
||||
* @return int[] Post IDs.
|
||||
*/
|
||||
private static function get_unenriched_ids(): array {
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => 'peptide',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'psa_half_life',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'psa_half_life',
|
||||
'value' => '',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total published peptide count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function get_total_published_count(): int {
|
||||
$counts = wp_count_posts( 'peptide' );
|
||||
return (int) ( $counts->publish ?? 0 );
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: Peptide Search AI
|
||||
* Plugin URI: https://example.com/peptide-search-ai
|
||||
* Description: Searchable peptide database with AI-powered auto-population and browsable directory.
|
||||
* Version: 4.3.0
|
||||
* Version: 4.4.0
|
||||
* Author: Terence
|
||||
* License: GPL v2 or later
|
||||
* Text Domain: peptide-search-ai
|
||||
@@ -15,7 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'PSA_VERSION', '4.3.0' );
|
||||
define( 'PSA_VERSION', '4.4.0' );
|
||||
define( 'PSA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'PSA_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'PSA_PLUGIN_FILE', __FILE__ );
|
||||
@@ -30,6 +30,7 @@ require_once PSA_PLUGIN_DIR . 'includes/class-psa-kb-builder.php';
|
||||
require_once PSA_PLUGIN_DIR . 'includes/class-psa-ai-generator.php';
|
||||
require_once PSA_PLUGIN_DIR . 'includes/class-psa-pubchem.php';
|
||||
require_once PSA_PLUGIN_DIR . 'includes/class-psa-admin.php';
|
||||
require_once PSA_PLUGIN_DIR . 'includes/class-psa-batch-enrichment.php';
|
||||
require_once PSA_PLUGIN_DIR . 'includes/class-psa-template.php';
|
||||
require_once PSA_PLUGIN_DIR . 'includes/class-psa-directory.php';
|
||||
|
||||
@@ -39,6 +40,7 @@ function psa_init() {
|
||||
PSA_Search::init();
|
||||
PSA_AI_Generator::init();
|
||||
PSA_Admin::init();
|
||||
PSA_Batch_Enrichment::init();
|
||||
PSA_Template::init();
|
||||
PSA_Directory::init();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user