feat: add structured plugin logging system with admin log viewer
Introduces Peptide_News_Logger class with leveled logging (info, warning, error, debug) to a custom DB table, auto-pruning at 2000 rows. Adds a filterable, paginated log viewer to the admin settings page. Instruments the LLM and Fetcher classes with structured log calls for fetch cycles, AI processing, and error tracking. Version bumped to 2.1.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -709,6 +709,131 @@ class Peptide_News_Admin {
|
|||||||
echo '<p><strong>' . esc_html__( 'Widget:', 'peptide-news' ) . '</strong> ' .
|
echo '<p><strong>' . esc_html__( 'Widget:', 'peptide-news' ) . '</strong> ' .
|
||||||
esc_html__( 'Add "Peptide News" from Appearance > Widgets.', 'peptide-news' ) . '</p>';
|
esc_html__( 'Add "Peptide News" from Appearance > Widgets.', 'peptide-news' ) . '</p>';
|
||||||
|
|
||||||
|
// ── Plugin Log Viewer ──────────────────────────────────────────
|
||||||
|
echo '<h2>' . esc_html__( 'Plugin Log', 'peptide-news' ) . '</h2>';
|
||||||
|
echo '<p class="description">' . esc_html__( 'Recent plugin activity — fetches, AI processing, errors, and admin actions.', 'peptide-news' ) . '</p>';
|
||||||
|
|
||||||
|
// Filters row.
|
||||||
|
echo '<div style="margin:10px 0;display:flex;gap:8px;align-items:center;">';
|
||||||
|
echo '<select id="peptide-log-level">';
|
||||||
|
echo '<option value="">' . esc_html__( 'All Levels', 'peptide-news' ) . '</option>';
|
||||||
|
echo '<option value="info">Info</option>';
|
||||||
|
echo '<option value="warning">Warning</option>';
|
||||||
|
echo '<option value="error">Error</option>';
|
||||||
|
echo '<option value="debug">Debug</option>';
|
||||||
|
echo '</select>';
|
||||||
|
echo '<select id="peptide-log-context">';
|
||||||
|
echo '<option value="">' . esc_html__( 'All Contexts', 'peptide-news' ) . '</option>';
|
||||||
|
echo '<option value="fetch">Fetch</option>';
|
||||||
|
echo '<option value="llm">LLM / AI</option>';
|
||||||
|
echo '<option value="cron">Cron</option>';
|
||||||
|
echo '<option value="admin">Admin</option>';
|
||||||
|
echo '<option value="general">General</option>';
|
||||||
|
echo '</select>';
|
||||||
|
echo '<button type="button" id="peptide-log-refresh" class="button">' . esc_html__( 'Refresh', 'peptide-news' ) . '</button>';
|
||||||
|
echo '<button type="button" id="peptide-log-clear" class="button" style="color:#a00;">' . esc_html__( 'Clear Log', 'peptide-news' ) . '</button>';
|
||||||
|
echo '<span id="peptide-log-status" style="margin-left:8px;color:#666;"></span>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// Log table.
|
||||||
|
echo '<div id="peptide-log-container" style="max-height:400px;overflow-y:auto;border:1px solid #c3c4c7;background:#fff;">';
|
||||||
|
echo '<table class="widefat striped" style="margin:0;">';
|
||||||
|
echo '<thead><tr>';
|
||||||
|
echo '<th style="width:140px;">' . esc_html__( 'Time', 'peptide-news' ) . '</th>';
|
||||||
|
echo '<th style="width:70px;">' . esc_html__( 'Level', 'peptide-news' ) . '</th>';
|
||||||
|
echo '<th style="width:70px;">' . esc_html__( 'Context', 'peptide-news' ) . '</th>';
|
||||||
|
echo '<th>' . esc_html__( 'Message', 'peptide-news' ) . '</th>';
|
||||||
|
echo '</tr></thead>';
|
||||||
|
echo '<tbody id="peptide-log-body"><tr><td colspan="4" style="text-align:center;color:#999;">' . esc_html__( 'Loading...', 'peptide-news' ) . '</td></tr></tbody>';
|
||||||
|
echo '</table>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// Pagination.
|
||||||
|
echo '<div id="peptide-log-pagination" style="margin:8px 0;display:flex;gap:8px;align-items:center;"></div>';
|
||||||
|
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
(function($) {
|
||||||
|
var currentPage = 1;
|
||||||
|
|
||||||
|
function levelBadge(level) {
|
||||||
|
var colors = { info: '#2271b1', warning: '#dba617', error: '#d63638', debug: '#8c8f94' };
|
||||||
|
var bg = { info: '#f0f6fc', warning: '#fcf9e8', error: '#fcf0f1', debug: '#f0f0f1' };
|
||||||
|
return '<span style="display:inline-block;padding:2px 8px;border-radius:3px;font-size:12px;font-weight:500;color:' +
|
||||||
|
(colors[level] || '#666') + ';background:' + (bg[level] || '#f0f0f1') + ';">' + level + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLogs(page) {
|
||||||
|
currentPage = page || 1;
|
||||||
|
var $body = $('#peptide-log-body');
|
||||||
|
$body.html('<tr><td colspan="4" style="text-align:center;color:#999;">Loading...</td></tr>');
|
||||||
|
|
||||||
|
$.post(peptideNewsAdmin.ajax_url, {
|
||||||
|
action: 'peptide_news_get_logs',
|
||||||
|
nonce: peptideNewsAdmin.admin_nonce,
|
||||||
|
page: currentPage,
|
||||||
|
level: $('#peptide-log-level').val(),
|
||||||
|
context: $('#peptide-log-context').val()
|
||||||
|
}, function(response) {
|
||||||
|
if (!response.success) {
|
||||||
|
$body.html('<tr><td colspan="4" style="color:#d63638;">Error loading logs.</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = response.data.rows;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
$body.html('<tr><td colspan="4" style="text-align:center;color:#999;">No log entries.</td></tr>');
|
||||||
|
$('#peptide-log-pagination').empty();
|
||||||
|
$('#peptide-log-status').text('0 entries');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
$.each(rows, function(i, row) {
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td style="font-size:12px;white-space:nowrap;">' + $('<span>').text(row.created_at).html() + '</td>';
|
||||||
|
html += '<td>' + levelBadge(row.level) + '</td>';
|
||||||
|
html += '<td style="font-size:12px;">' + $('<span>').text(row.context).html() + '</td>';
|
||||||
|
html += '<td style="font-size:13px;word-break:break-word;">' + $('<span>').text(row.message).html() + '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
$body.html(html);
|
||||||
|
|
||||||
|
// Pagination.
|
||||||
|
var totalPages = response.data.total_pages;
|
||||||
|
var pagHtml = '<span style="color:#666;font-size:12px;">Page ' + currentPage + ' of ' + totalPages + ' (' + response.data.total + ' entries)</span>';
|
||||||
|
if (currentPage > 1) {
|
||||||
|
pagHtml += ' <button type="button" class="button button-small peptide-log-page" data-page="' + (currentPage - 1) + '">« Prev</button>';
|
||||||
|
}
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
pagHtml += ' <button type="button" class="button button-small peptide-log-page" data-page="' + (currentPage + 1) + '">Next »</button>';
|
||||||
|
}
|
||||||
|
$('#peptide-log-pagination').html(pagHtml);
|
||||||
|
$('#peptide-log-status').text(response.data.total + ' entries');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bindings.
|
||||||
|
$('#peptide-log-refresh').on('click', function() { loadLogs(1); });
|
||||||
|
$('#peptide-log-level, #peptide-log-context').on('change', function() { loadLogs(1); });
|
||||||
|
$(document).on('click', '.peptide-log-page', function() { loadLogs($(this).data('page')); });
|
||||||
|
|
||||||
|
$('#peptide-log-clear').on('click', function() {
|
||||||
|
if (!confirm('<?php echo esc_js( __( 'Clear all log entries?', 'peptide-news' ) ); ?>')) return;
|
||||||
|
$.post(peptideNewsAdmin.ajax_url, {
|
||||||
|
action: 'peptide_news_clear_logs',
|
||||||
|
nonce: peptideNewsAdmin.admin_nonce
|
||||||
|
}, function(response) {
|
||||||
|
if (response.success) loadLogs(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load.
|
||||||
|
loadLogs(1);
|
||||||
|
})(jQuery);
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class Peptide_News_Activator {
|
|||||||
self::create_tables();
|
self::create_tables();
|
||||||
self::set_default_options();
|
self::set_default_options();
|
||||||
self::schedule_cron();
|
self::schedule_cron();
|
||||||
|
|
||||||
|
// Create the log table (idempotent via dbDelta).
|
||||||
|
if ( class_exists( 'Peptide_News_Logger' ) ) {
|
||||||
|
Peptide_News_Logger::create_table();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,11 +44,13 @@ class Peptide_News_Fetcher {
|
|||||||
// Prevent overlapping fetch jobs.
|
// Prevent overlapping fetch jobs.
|
||||||
$lock_key = 'peptide_news_fetch_lock';
|
$lock_key = 'peptide_news_fetch_lock';
|
||||||
if ( get_transient( $lock_key ) ) {
|
if ( get_transient( $lock_key ) ) {
|
||||||
$this->log_error( 'Fetch already in progress. Skipping this cycle.' );
|
Peptide_News_Logger::warning( 'Fetch already in progress — skipping this cycle.', 'fetch' );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
set_transient( $lock_key, true, 300 ); // 5-minute lock.
|
set_transient( $lock_key, true, 300 ); // 5-minute lock.
|
||||||
|
|
||||||
|
Peptide_News_Logger::info( 'Fetch cycle started.', 'fetch' );
|
||||||
|
|
||||||
$articles = array();
|
$articles = array();
|
||||||
|
|
||||||
// Fetch from RSS feeds.
|
// Fetch from RSS feeds.
|
||||||
@@ -89,6 +91,11 @@ class Peptide_News_Fetcher {
|
|||||||
'new_stored' => $stored,
|
'new_stored' => $stored,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
|
Peptide_News_Logger::info( sprintf(
|
||||||
|
'Fetch complete: %d found, %d filtered out, %d new stored.',
|
||||||
|
$pre_filter_count, $filtered_out, $stored
|
||||||
|
), 'fetch' );
|
||||||
|
|
||||||
// Run AI analysis on newly fetched articles (keywords + summary).
|
// Run AI analysis on newly fetched articles (keywords + summary).
|
||||||
if ( class_exists( 'Peptide_News_LLM' ) && Peptide_News_LLM::is_enabled() ) {
|
if ( class_exists( 'Peptide_News_LLM' ) && Peptide_News_LLM::is_enabled() ) {
|
||||||
$llm_batch_size = absint( get_option( 'peptide_news_llm_max_articles', 10 ) );
|
$llm_batch_size = absint( get_option( 'peptide_news_llm_max_articles', 10 ) );
|
||||||
|
|||||||
@@ -75,14 +75,15 @@ class Peptide_News_LLM {
|
|||||||
if ( ! is_wp_error( $keywords_result ) ) {
|
if ( ! is_wp_error( $keywords_result ) ) {
|
||||||
$results['keywords'] = self::sanitize_keywords( $keywords_result );
|
$results['keywords'] = self::sanitize_keywords( $keywords_result );
|
||||||
$any_success = true;
|
$any_success = true;
|
||||||
|
Peptide_News_Logger::debug( 'Keywords extracted for article #' . $article->id, 'llm' );
|
||||||
} else {
|
} else {
|
||||||
$err_msg = 'Keywords (' . $keywords_model . '): ' . $keywords_result->get_error_message();
|
$err_msg = 'Keywords (' . $keywords_model . '): ' . $keywords_result->get_error_message();
|
||||||
$results['errors'][] = $err_msg;
|
$results['errors'][] = $err_msg;
|
||||||
self::log_error( 'Keyword extraction failed for article ' . $article->id . ': ' . $keywords_result->get_error_message() );
|
Peptide_News_Logger::error( 'Keyword extraction failed for article #' . $article->id . ': ' . $keywords_result->get_error_message(), 'llm' );
|
||||||
}
|
}
|
||||||
} elseif ( $need_keywords ) {
|
} elseif ( $need_keywords ) {
|
||||||
$results['errors'][] = 'Invalid keywords model ID: ' . $keywords_model;
|
$results['errors'][] = 'Invalid keywords model ID: ' . $keywords_model;
|
||||||
self::log_error( 'Invalid keywords model ID: ' . $keywords_model );
|
Peptide_News_Logger::error( 'Invalid keywords model ID: ' . $keywords_model, 'llm' );
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Summarization ---
|
// --- Summarization ---
|
||||||
@@ -97,14 +98,15 @@ class Peptide_News_LLM {
|
|||||||
// Strip all HTML tags from LLM output to prevent XSS.
|
// Strip all HTML tags from LLM output to prevent XSS.
|
||||||
$results['summary'] = sanitize_textarea_field( wp_strip_all_tags( $summary_result ) );
|
$results['summary'] = sanitize_textarea_field( wp_strip_all_tags( $summary_result ) );
|
||||||
$any_success = true;
|
$any_success = true;
|
||||||
|
Peptide_News_Logger::info( 'AI summary generated for article #' . $article->id . ': ' . mb_substr( $article->title, 0, 60 ), 'llm' );
|
||||||
} else {
|
} else {
|
||||||
$err_msg = 'Summary (' . $summary_model . '): ' . $summary_result->get_error_message();
|
$err_msg = 'Summary (' . $summary_model . '): ' . $summary_result->get_error_message();
|
||||||
$results['errors'][] = $err_msg;
|
$results['errors'][] = $err_msg;
|
||||||
self::log_error( 'Summarization failed for article ' . $article->id . ': ' . $summary_result->get_error_message() );
|
Peptide_News_Logger::error( 'Summarization failed for article #' . $article->id . ': ' . $summary_result->get_error_message(), 'llm' );
|
||||||
}
|
}
|
||||||
} elseif ( $need_summary ) {
|
} elseif ( $need_summary ) {
|
||||||
$results['errors'][] = 'Invalid summary model ID: ' . $summary_model;
|
$results['errors'][] = 'Invalid summary model ID: ' . $summary_model;
|
||||||
self::log_error( 'Invalid summary model ID: ' . $summary_model );
|
Peptide_News_Logger::error( 'Invalid summary model ID: ' . $summary_model, 'llm' );
|
||||||
}
|
}
|
||||||
|
|
||||||
$results['success'] = $any_success;
|
$results['success'] = $any_success;
|
||||||
@@ -198,6 +200,12 @@ class Peptide_News_LLM {
|
|||||||
'errors' => $last_errors,
|
'errors' => $last_errors,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
|
if ( $processed > 0 ) {
|
||||||
|
Peptide_News_Logger::info( sprintf( 'AI batch complete: %d/%d articles processed.', $processed, count( $articles ) ), 'llm' );
|
||||||
|
} elseif ( count( $articles ) > 0 ) {
|
||||||
|
Peptide_News_Logger::warning( sprintf( 'AI batch: 0/%d articles succeeded.', count( $articles ) ), 'llm' );
|
||||||
|
}
|
||||||
|
|
||||||
return $processed;
|
return $processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +415,8 @@ class Peptide_News_LLM {
|
|||||||
wp_send_json_error( 'AI Analysis is not enabled. Please enable it and set an OpenRouter API key in Peptide News → Settings.' );
|
wp_send_json_error( 'AI Analysis is not enabled. Please enable it and set an OpenRouter API key in Peptide News → Settings.' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Peptide_News_Logger::info( 'Bulk AI summary generation triggered by admin.', 'llm' );
|
||||||
|
|
||||||
// Auto-correct known broken model names to a working free model.
|
// Auto-correct known broken model names to a working free model.
|
||||||
$broken_models = array(
|
$broken_models = array(
|
||||||
'qwen/qwen3.6-plus:free',
|
'qwen/qwen3.6-plus:free',
|
||||||
|
|||||||
254
includes/class-peptide-news-logger.php
Normal file
254
includes/class-peptide-news-logger.php
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Logger.
|
||||||
|
*
|
||||||
|
* Provides structured logging to a custom database table with levels,
|
||||||
|
* contexts, and an admin-facing viewer with AJAX pagination and clear.
|
||||||
|
*
|
||||||
|
* @since 2.1.0
|
||||||
|
*/
|
||||||
|
class Peptide_News_Logger {
|
||||||
|
|
||||||
|
/** Log level constants. */
|
||||||
|
const LEVEL_INFO = 'info';
|
||||||
|
const LEVEL_WARNING = 'warning';
|
||||||
|
const LEVEL_ERROR = 'error';
|
||||||
|
const LEVEL_DEBUG = 'debug';
|
||||||
|
|
||||||
|
/** Maximum rows kept in the log table (auto-pruned on write). */
|
||||||
|
const MAX_ROWS = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the log table name.
|
||||||
|
*/
|
||||||
|
public static function table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . 'peptide_news_log';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Writing
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a log entry.
|
||||||
|
*
|
||||||
|
* @param string $level One of the LEVEL_* constants.
|
||||||
|
* @param string $message Human-readable message.
|
||||||
|
* @param string $context Category tag (fetch, llm, admin, cron, general).
|
||||||
|
*/
|
||||||
|
public static function log( $level, $message, $context = 'general' ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
self::table(),
|
||||||
|
array(
|
||||||
|
'level' => $level,
|
||||||
|
'context' => $context,
|
||||||
|
'message' => $message,
|
||||||
|
'created_at' => current_time( 'mysql' ),
|
||||||
|
),
|
||||||
|
array( '%s', '%s', '%s', '%s' )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-prune old entries.
|
||||||
|
self::maybe_prune();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: info level. */
|
||||||
|
public static function info( $message, $context = 'general' ) {
|
||||||
|
self::log( self::LEVEL_INFO, $message, $context );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: warning level. */
|
||||||
|
public static function warning( $message, $context = 'general' ) {
|
||||||
|
self::log( self::LEVEL_WARNING, $message, $context );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: error level. */
|
||||||
|
public static function error( $message, $context = 'general' ) {
|
||||||
|
self::log( self::LEVEL_ERROR, $message, $context );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: debug level (only writes when WP_DEBUG is on). */
|
||||||
|
public static function debug( $message, $context = 'general' ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
self::log( self::LEVEL_DEBUG, $message, $context );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Reading
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve log entries.
|
||||||
|
*
|
||||||
|
* @param array $args {
|
||||||
|
* Optional. Query arguments.
|
||||||
|
*
|
||||||
|
* @type string $level Filter by level.
|
||||||
|
* @type string $context Filter by context.
|
||||||
|
* @type int $limit Number of rows.
|
||||||
|
* @type int $offset Pagination offset.
|
||||||
|
* }
|
||||||
|
* @return array { 'rows' => array, 'total' => int }
|
||||||
|
*/
|
||||||
|
public static function get_logs( $args = array() ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$defaults = array(
|
||||||
|
'level' => '',
|
||||||
|
'context' => '',
|
||||||
|
'limit' => 50,
|
||||||
|
'offset' => 0,
|
||||||
|
);
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
|
||||||
|
$table = self::table();
|
||||||
|
$where = array( '1=1' );
|
||||||
|
$vals = array();
|
||||||
|
|
||||||
|
if ( ! empty( $args['level'] ) ) {
|
||||||
|
$where[] = 'level = %s';
|
||||||
|
$vals[] = $args['level'];
|
||||||
|
}
|
||||||
|
if ( ! empty( $args['context'] ) ) {
|
||||||
|
$where[] = 'context = %s';
|
||||||
|
$vals[] = $args['context'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_sql = implode( ' AND ', $where );
|
||||||
|
|
||||||
|
// Total count.
|
||||||
|
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||||
|
$total = empty( $vals )
|
||||||
|
? (int) $wpdb->get_var( $count_sql )
|
||||||
|
: (int) $wpdb->get_var( $wpdb->prepare( $count_sql, $vals ) );
|
||||||
|
|
||||||
|
// Rows.
|
||||||
|
$order_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d";
|
||||||
|
$vals[] = absint( $args['limit'] );
|
||||||
|
$vals[] = absint( $args['offset'] );
|
||||||
|
$rows = $wpdb->get_results( $wpdb->prepare( $order_sql, $vals ) );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'rows' => $rows ? $rows : array(),
|
||||||
|
'total' => $total,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Maintenance
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove rows beyond the MAX_ROWS cap.
|
||||||
|
*/
|
||||||
|
private static function maybe_prune() {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::table();
|
||||||
|
|
||||||
|
// Only prune every ~50 writes (probabilistic).
|
||||||
|
if ( wp_rand( 1, 50 ) !== 1 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
|
||||||
|
|
||||||
|
if ( $count > self::MAX_ROWS ) {
|
||||||
|
$delete_count = $count - self::MAX_ROWS;
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"DELETE FROM {$table} ORDER BY id ASC LIMIT %d",
|
||||||
|
$delete_count
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all log entries.
|
||||||
|
*/
|
||||||
|
public static function clear() {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query( "TRUNCATE TABLE " . self::table() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* AJAX handlers
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: fetch paginated log entries.
|
||||||
|
*/
|
||||||
|
public static function ajax_get_logs() {
|
||||||
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( 'Unauthorized', 403 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
||||||
|
$level = isset( $_POST['level'] ) ? sanitize_text_field( $_POST['level'] ) : '';
|
||||||
|
$context = isset( $_POST['context'] ) ? sanitize_text_field( $_POST['context'] ) : '';
|
||||||
|
$per_page = 50;
|
||||||
|
|
||||||
|
$result = self::get_logs( array(
|
||||||
|
'level' => $level,
|
||||||
|
'context' => $context,
|
||||||
|
'limit' => $per_page,
|
||||||
|
'offset' => ( $page - 1 ) * $per_page,
|
||||||
|
) );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'rows' => $result['rows'],
|
||||||
|
'total' => $result['total'],
|
||||||
|
'page' => $page,
|
||||||
|
'total_pages' => ceil( $result['total'] / $per_page ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: clear all log entries.
|
||||||
|
*/
|
||||||
|
public static function ajax_clear_logs() {
|
||||||
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( 'Unauthorized', 403 );
|
||||||
|
}
|
||||||
|
|
||||||
|
self::clear();
|
||||||
|
self::info( 'Log cleared by admin.', 'admin' );
|
||||||
|
|
||||||
|
wp_send_json_success( array( 'message' => 'Log cleared.' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Schema
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the log table (called from activator / upgrade check).
|
||||||
|
*/
|
||||||
|
public static function create_table() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = self::table();
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE {$table} (
|
||||||
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
level VARCHAR(10) NOT NULL DEFAULT 'info',
|
||||||
|
context VARCHAR(30) NOT NULL DEFAULT 'general',
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_level (level),
|
||||||
|
KEY idx_context (context),
|
||||||
|
KEY idx_created (created_at)
|
||||||
|
) {$charset_collate};";
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
dbDelta( $sql );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ class Peptide_News {
|
|||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-api.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-api.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-llm.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-llm.php';
|
||||||
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-logger.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-content-filter.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-content-filter.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'admin/class-peptide-news-admin.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'admin/class-peptide-news-admin.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'public/class-peptide-news-public.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'public/class-peptide-news-public.php';
|
||||||
@@ -82,6 +83,8 @@ class Peptide_News {
|
|||||||
$this->loader->add_filter( 'cron_schedules', $fetcher, 'add_custom_cron_schedules' );
|
$this->loader->add_filter( 'cron_schedules', $fetcher, 'add_custom_cron_schedules' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_backfill_sources', $fetcher, 'ajax_backfill_sources' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_backfill_sources', $fetcher, 'ajax_backfill_sources' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_generate_summaries', 'Peptide_News_LLM', 'ajax_generate_summaries' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_generate_summaries', 'Peptide_News_LLM', 'ajax_generate_summaries' );
|
||||||
|
$this->loader->add_action( 'wp_ajax_peptide_news_get_logs', 'Peptide_News_Logger', 'ajax_get_logs' );
|
||||||
|
$this->loader->add_action( 'wp_ajax_peptide_news_clear_logs', 'Peptide_News_Logger', 'ajax_clear_logs' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Peptide News Aggregator
|
* Plugin Name: Peptide News Aggregator
|
||||||
* Plugin URI: https://github.com/peptiderepo/peptide-news-plugin
|
* Plugin URI: https://github.com/peptiderepo/peptide-news-plugin
|
||||||
* Description: Aggregates and displays the latest peptide research news from multiple sources with click analytics and trend reporting.
|
* Description: Aggregates and displays the latest peptide research news from multiple sources with click analytics and trend reporting.
|
||||||
* Version: 2.0.1
|
* Version: 2.1.0
|
||||||
* Author: Peptide News Team
|
* Author: Peptide News Team
|
||||||
* Author URI: https://github.com/peptiderepo
|
* Author URI: https://github.com/peptiderepo
|
||||||
* License: GPL-2.0+
|
* License: GPL-2.0+
|
||||||
@@ -22,7 +22,7 @@ if ( ! defined( 'WPINC' ) ) {
|
|||||||
/**
|
/**
|
||||||
* Current plugin version — follows SemVer.
|
* Current plugin version — follows SemVer.
|
||||||
*/
|
*/
|
||||||
define( 'PEPTIDE_NEWS_VERSION', '2.0.1' );
|
define( 'PEPTIDE_NEWS_VERSION', '2.1.0' );
|
||||||
define( 'PEPTIDE_NEWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
define( 'PEPTIDE_NEWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'PEPTIDE_NEWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
define( 'PEPTIDE_NEWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
define( 'PEPTIDE_NEWS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
define( 'PEPTIDE_NEWS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||||
@@ -60,6 +60,7 @@ function peptide_news_check_db_upgrade() {
|
|||||||
$installed_version = get_option( 'peptide_news_db_version', '1.0.0' );
|
$installed_version = get_option( 'peptide_news_db_version', '1.0.0' );
|
||||||
if ( version_compare( $installed_version, PEPTIDE_NEWS_VERSION, '<' ) ) {
|
if ( version_compare( $installed_version, PEPTIDE_NEWS_VERSION, '<' ) ) {
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-activator.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-activator.php';
|
||||||
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-logger.php';
|
||||||
Peptide_News_Activator::activate();
|
Peptide_News_Activator::activate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -709,6 +709,131 @@ class Peptide_News_Admin {
|
|||||||
echo '<p><strong>' . esc_html__( 'Widget:', 'peptide-news' ) . '</strong> ' .
|
echo '<p><strong>' . esc_html__( 'Widget:', 'peptide-news' ) . '</strong> ' .
|
||||||
esc_html__( 'Add "Peptide News" from Appearance > Widgets.', 'peptide-news' ) . '</p>';
|
esc_html__( 'Add "Peptide News" from Appearance > Widgets.', 'peptide-news' ) . '</p>';
|
||||||
|
|
||||||
|
// ── Plugin Log Viewer ──────────────────────────────────────────
|
||||||
|
echo '<h2>' . esc_html__( 'Plugin Log', 'peptide-news' ) . '</h2>';
|
||||||
|
echo '<p class="description">' . esc_html__( 'Recent plugin activity — fetches, AI processing, errors, and admin actions.', 'peptide-news' ) . '</p>';
|
||||||
|
|
||||||
|
// Filters row.
|
||||||
|
echo '<div style="margin:10px 0;display:flex;gap:8px;align-items:center;">';
|
||||||
|
echo '<select id="peptide-log-level">';
|
||||||
|
echo '<option value="">' . esc_html__( 'All Levels', 'peptide-news' ) . '</option>';
|
||||||
|
echo '<option value="info">Info</option>';
|
||||||
|
echo '<option value="warning">Warning</option>';
|
||||||
|
echo '<option value="error">Error</option>';
|
||||||
|
echo '<option value="debug">Debug</option>';
|
||||||
|
echo '</select>';
|
||||||
|
echo '<select id="peptide-log-context">';
|
||||||
|
echo '<option value="">' . esc_html__( 'All Contexts', 'peptide-news' ) . '</option>';
|
||||||
|
echo '<option value="fetch">Fetch</option>';
|
||||||
|
echo '<option value="llm">LLM / AI</option>';
|
||||||
|
echo '<option value="cron">Cron</option>';
|
||||||
|
echo '<option value="admin">Admin</option>';
|
||||||
|
echo '<option value="general">General</option>';
|
||||||
|
echo '</select>';
|
||||||
|
echo '<button type="button" id="peptide-log-refresh" class="button">' . esc_html__( 'Refresh', 'peptide-news' ) . '</button>';
|
||||||
|
echo '<button type="button" id="peptide-log-clear" class="button" style="color:#a00;">' . esc_html__( 'Clear Log', 'peptide-news' ) . '</button>';
|
||||||
|
echo '<span id="peptide-log-status" style="margin-left:8px;color:#666;"></span>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// Log table.
|
||||||
|
echo '<div id="peptide-log-container" style="max-height:400px;overflow-y:auto;border:1px solid #c3c4c7;background:#fff;">';
|
||||||
|
echo '<table class="widefat striped" style="margin:0;">';
|
||||||
|
echo '<thead><tr>';
|
||||||
|
echo '<th style="width:140px;">' . esc_html__( 'Time', 'peptide-news' ) . '</th>';
|
||||||
|
echo '<th style="width:70px;">' . esc_html__( 'Level', 'peptide-news' ) . '</th>';
|
||||||
|
echo '<th style="width:70px;">' . esc_html__( 'Context', 'peptide-news' ) . '</th>';
|
||||||
|
echo '<th>' . esc_html__( 'Message', 'peptide-news' ) . '</th>';
|
||||||
|
echo '</tr></thead>';
|
||||||
|
echo '<tbody id="peptide-log-body"><tr><td colspan="4" style="text-align:center;color:#999;">' . esc_html__( 'Loading...', 'peptide-news' ) . '</td></tr></tbody>';
|
||||||
|
echo '</table>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// Pagination.
|
||||||
|
echo '<div id="peptide-log-pagination" style="margin:8px 0;display:flex;gap:8px;align-items:center;"></div>';
|
||||||
|
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
(function($) {
|
||||||
|
var currentPage = 1;
|
||||||
|
|
||||||
|
function levelBadge(level) {
|
||||||
|
var colors = { info: '#2271b1', warning: '#dba617', error: '#d63638', debug: '#8c8f94' };
|
||||||
|
var bg = { info: '#f0f6fc', warning: '#fcf9e8', error: '#fcf0f1', debug: '#f0f0f1' };
|
||||||
|
return '<span style="display:inline-block;padding:2px 8px;border-radius:3px;font-size:12px;font-weight:500;color:' +
|
||||||
|
(colors[level] || '#666') + ';background:' + (bg[level] || '#f0f0f1') + ';">' + level + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLogs(page) {
|
||||||
|
currentPage = page || 1;
|
||||||
|
var $body = $('#peptide-log-body');
|
||||||
|
$body.html('<tr><td colspan="4" style="text-align:center;color:#999;">Loading...</td></tr>');
|
||||||
|
|
||||||
|
$.post(peptideNewsAdmin.ajax_url, {
|
||||||
|
action: 'peptide_news_get_logs',
|
||||||
|
nonce: peptideNewsAdmin.admin_nonce,
|
||||||
|
page: currentPage,
|
||||||
|
level: $('#peptide-log-level').val(),
|
||||||
|
context: $('#peptide-log-context').val()
|
||||||
|
}, function(response) {
|
||||||
|
if (!response.success) {
|
||||||
|
$body.html('<tr><td colspan="4" style="color:#d63638;">Error loading logs.</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = response.data.rows;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
$body.html('<tr><td colspan="4" style="text-align:center;color:#999;">No log entries.</td></tr>');
|
||||||
|
$('#peptide-log-pagination').empty();
|
||||||
|
$('#peptide-log-status').text('0 entries');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
$.each(rows, function(i, row) {
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td style="font-size:12px;white-space:nowrap;">' + $('<span>').text(row.created_at).html() + '</td>';
|
||||||
|
html += '<td>' + levelBadge(row.level) + '</td>';
|
||||||
|
html += '<td style="font-size:12px;">' + $('<span>').text(row.context).html() + '</td>';
|
||||||
|
html += '<td style="font-size:13px;word-break:break-word;">' + $('<span>').text(row.message).html() + '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
$body.html(html);
|
||||||
|
|
||||||
|
// Pagination.
|
||||||
|
var totalPages = response.data.total_pages;
|
||||||
|
var pagHtml = '<span style="color:#666;font-size:12px;">Page ' + currentPage + ' of ' + totalPages + ' (' + response.data.total + ' entries)</span>';
|
||||||
|
if (currentPage > 1) {
|
||||||
|
pagHtml += ' <button type="button" class="button button-small peptide-log-page" data-page="' + (currentPage - 1) + '">« Prev</button>';
|
||||||
|
}
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
pagHtml += ' <button type="button" class="button button-small peptide-log-page" data-page="' + (currentPage + 1) + '">Next »</button>';
|
||||||
|
}
|
||||||
|
$('#peptide-log-pagination').html(pagHtml);
|
||||||
|
$('#peptide-log-status').text(response.data.total + ' entries');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bindings.
|
||||||
|
$('#peptide-log-refresh').on('click', function() { loadLogs(1); });
|
||||||
|
$('#peptide-log-level, #peptide-log-context').on('change', function() { loadLogs(1); });
|
||||||
|
$(document).on('click', '.peptide-log-page', function() { loadLogs($(this).data('page')); });
|
||||||
|
|
||||||
|
$('#peptide-log-clear').on('click', function() {
|
||||||
|
if (!confirm('<?php echo esc_js( __( 'Clear all log entries?', 'peptide-news' ) ); ?>')) return;
|
||||||
|
$.post(peptideNewsAdmin.ajax_url, {
|
||||||
|
action: 'peptide_news_clear_logs',
|
||||||
|
nonce: peptideNewsAdmin.admin_nonce
|
||||||
|
}, function(response) {
|
||||||
|
if (response.success) loadLogs(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load.
|
||||||
|
loadLogs(1);
|
||||||
|
})(jQuery);
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,36 +2,146 @@
|
|||||||
/**
|
/**
|
||||||
* Fired during plugin activation.
|
* Fired during plugin activation.
|
||||||
*
|
*
|
||||||
* Create database tables and dispatch activation hooks.
|
* Creates database tables and schedules the cron event.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Peptide_News_Activator {
|
class Peptide_News_Activator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom tables and schedule cron.
|
||||||
|
*/
|
||||||
public static function activate() {
|
public static function activate() {
|
||||||
global $wpdb;
|
self::create_tables();
|
||||||
|
self::set_default_options();
|
||||||
|
self::schedule_cron();
|
||||||
|
|
||||||
// Create tables.
|
// Create the log table (idempotent via dbDelta).
|
||||||
require_once abspath(__FILE__, '../includes/class-peptide-news-activator.php');
|
if ( class_exists( 'Peptide_News_Logger' ) ) {
|
||||||
|
Peptide_News_Logger::create_table();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the articles and analytics database tables.
|
||||||
|
*/
|
||||||
|
private static function create_tables() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
$charset_collate = $wpdb->get_charset_collate();
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
// Create articles table.
|
// Articles table — stores fetched news data for analysis.
|
||||||
$wpdb->query( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}peptide_news_articles (
|
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
ID bigint(19) UNSIGNED AUTO_INCREMENT,
|
$sql_articles = "CREATE TABLE {$articles_table} (
|
||||||
title varchar(255),
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
excerpt LONGTEXT,
|
source VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
content LONGTEXT,
|
source_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
source varchar(255),
|
title TEXT NOT NULL,
|
||||||
author varchar(255),
|
excerpt TEXT NOT NULL,
|
||||||
thumbnail varchar(500),
|
content LONGTEXT NOT NULL,
|
||||||
source_url text,
|
author VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
category_id bigint(20),
|
thumbnail_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
published_at datetime,
|
thumbnail_local VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
fetched_at datetime,
|
published_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (ID)
|
fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
){ $upload_file_name}{" WITH PARTITION (TITLE, STTUS;
|
categories VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
* INTEGER ", DROP TIE VABLOCKED IF EXISTS"} WITH (TEMP=da2) ");
|
tags VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
language VARCHAR(10) NOT NULL DEFAULT 'en',
|
||||||
|
sentiment_score FLOAT DEFAULT NULL,
|
||||||
|
ai_summary TEXT NOT NULL,
|
||||||
|
hash VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY hash (hash),
|
||||||
|
KEY idx_published (published_at),
|
||||||
|
KEY idx_source (source),
|
||||||
|
KEY idx_active_published (is_active, published_at)
|
||||||
|
) {$charset_collate};";
|
||||||
|
|
||||||
|
// Click analytics table — one row per click event.
|
||||||
|
$clicks_table = $wpdb->prefix . 'peptide_news_clicks';
|
||||||
|
$sql_clicks = "CREATE TABLE {$clicks_table} (
|
||||||
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
article_id BIGINT(20) UNSIGNED NOT NULL,
|
||||||
|
clicked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_ip VARCHAR(45) NOT NULL DEFAULT '',
|
||||||
|
user_agent TEXT NOT NULL,
|
||||||
|
referrer_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
page_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
session_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
user_id BIGINT(20) UNSIGNED DEFAULT NULL,
|
||||||
|
country VARCHAR(2) NOT NULL DEFAULT '',
|
||||||
|
device_type VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_article (article_id),
|
||||||
|
KEY idx_clicked_at (clicked_at),
|
||||||
|
KEY idx_session (session_id),
|
||||||
|
KEY idx_article_date (article_id, clicked_at)
|
||||||
|
) {$charset_collate};";
|
||||||
|
|
||||||
|
// Daily aggregates table — for fast trend queries.
|
||||||
|
$daily_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||||
|
$sql_daily = "CREATE TABLE {$daily_table} (
|
||||||
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
article_id BIGINT(20) UNSIGNED NOT NULL,
|
||||||
|
stat_date DATE NOT NULL,
|
||||||
|
click_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
unique_visitors INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY idx_article_date (article_id, stat_date),
|
||||||
|
KEY idx_date (stat_date)
|
||||||
|
) {$charset_collate};";
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
|
||||||
|
dbDelta( $sql_articles );
|
||||||
|
dbDelta( $sql_clicks );
|
||||||
|
dbDelta( $sql_daily );
|
||||||
|
|
||||||
|
update_option( 'peptide_news_db_version', PEPTIDE_NEWS_VERSION );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set sensible default option values.
|
||||||
|
*/
|
||||||
|
private static function set_default_options() {
|
||||||
|
$defaults = array(
|
||||||
|
'fetch_interval' => 'twicedaily',
|
||||||
|
'articles_count' => 10,
|
||||||
|
'newsapi_key' => '',
|
||||||
|
'newsapi_enabled' => 0,
|
||||||
|
'rss_feeds' => implode( "\n", array(
|
||||||
|
'https://news.google.com/rss/search?q=peptide+research&hl=en-US&gl=US&ceid=US:en',
|
||||||
|
'https://pubmed.ncbi.nlm.nih.gov/rss/search/1sUlVGjEXm1YN9QiGPblv_-bwB51JCtyLwVX85CVGDKCHFFR9Q/?limit=20&utm_campaign=pubmed-2&fc=20220101000000',
|
||||||
|
) ),
|
||||||
|
'rss_enabled' => 1,
|
||||||
|
'search_keywords' => 'peptide, peptides, peptide therapy, peptide research, BPC-157, Thymosin, GHK-Cu',
|
||||||
|
'thumbnail_fallback' => PEPTIDE_NEWS_PLUGIN_URL . 'public/images/default-thumb.png',
|
||||||
|
'article_retention' => 90,
|
||||||
|
'analytics_retention' => 365,
|
||||||
|
'anonymize_ip' => 1,
|
||||||
|
'llm_enabled' => 0,
|
||||||
|
'openrouter_api_key' => '',
|
||||||
|
'llm_keywords_model' => 'google/gemini-2.0-flash-001',
|
||||||
|
'llm_summary_model' => 'google/gemini-2.0-flash-001',
|
||||||
|
'llm_max_articles' => 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $defaults as $key => $value ) {
|
||||||
|
if ( false === get_option( "peptide_news_{$key}" ) ) {
|
||||||
|
add_option( "peptide_news_{$key}", $value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule the cron event based on the configured interval.
|
||||||
|
*/
|
||||||
|
private static function schedule_cron() {
|
||||||
|
$interval = get_option( 'peptide_news_fetch_interval', 'twicedaily' );
|
||||||
|
|
||||||
|
if ( ! wp_next_scheduled( 'peptide_news_cron_fetch' ) ) {
|
||||||
|
wp_schedule_event( time(), $interval, 'peptide_news_cron_fetch' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,544 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Fetcher class for fetching articles from NewsAPI TöOa. and RSS feeds.
|
* Fetches news from multiple sources and stores them in the database.
|
||||||
*
|
*
|
||||||
* Fetches articles from configured sources and stores them in the database.
|
* Supported sources:
|
||||||
|
* - RSS / Atom feeds (Google News, PubMed, custom)
|
||||||
|
* - NewsAPI.org
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Peptide_News_Fetcher {
|
class Peptide_News_Fetcher {
|
||||||
|
|
||||||
public function fetch() {
|
/**
|
||||||
|
* Add custom WP-Cron schedules.
|
||||||
|
*
|
||||||
|
* @param array $schedules Existing schedules.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_custom_cron_schedules( $schedules ) {
|
||||||
|
$schedules['every_fifteen_minutes'] = array(
|
||||||
|
'interval' => 900,
|
||||||
|
'display' => __( 'Every 15 Minutes', 'peptide-news' ),
|
||||||
|
);
|
||||||
|
$schedules['every_thirty_minutes'] = array(
|
||||||
|
'interval' => 1800,
|
||||||
|
'display' => __( 'Every 30 Minutes', 'peptide-news' ),
|
||||||
|
);
|
||||||
|
$schedules['every_four_hours'] = array(
|
||||||
|
'interval' => 14400,
|
||||||
|
'display' => __( 'Every 4 Hours', 'peptide-news' ),
|
||||||
|
);
|
||||||
|
$schedules['every_six_hours'] = array(
|
||||||
|
'interval' => 21600,
|
||||||
|
'display' => __( 'Every 6 Hours', 'peptide-news' ),
|
||||||
|
);
|
||||||
|
return $schedules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master fetch method — called by WP-Cron.
|
||||||
|
* Uses a transient lock to prevent concurrent executions.
|
||||||
|
*/
|
||||||
|
public function fetch_all_sources() {
|
||||||
|
// Prevent overlapping fetch jobs.
|
||||||
|
$lock_key = 'peptide_news_fetch_lock';
|
||||||
|
if ( get_transient( $lock_key ) ) {
|
||||||
|
Peptide_News_Logger::warning( 'Fetch already in progress — skipping this cycle.', 'fetch' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set_transient( $lock_key, true, 300 ); // 5-minute lock.
|
||||||
|
|
||||||
|
Peptide_News_Logger::info( 'Fetch cycle started.', 'fetch' );
|
||||||
|
|
||||||
|
$articles = array();
|
||||||
|
|
||||||
|
// Fetch from RSS feeds.
|
||||||
|
if ( get_option( 'peptide_news_rss_enabled', 1 ) ) {
|
||||||
|
$rss_articles = $this->fetch_rss_feeds();
|
||||||
|
$articles = array_merge( $articles, $rss_articles );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from NewsAPI.
|
||||||
|
if ( get_option( 'peptide_news_newsapi_enabled', 0 ) ) {
|
||||||
|
$api_key = get_option( 'peptide_news_newsapi_key', '' );
|
||||||
|
if ( ! empty( $api_key ) ) {
|
||||||
|
$newsapi_articles = $this->fetch_newsapi( $api_key );
|
||||||
|
$articles = array_merge( $articles, $newsapi_articles );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out ads, press releases, and promotional content.
|
||||||
|
$pre_filter_count = count( $articles );
|
||||||
|
if ( class_exists( 'Peptide_News_Content_Filter' ) && Peptide_News_Content_Filter::is_enabled() ) {
|
||||||
|
$articles = Peptide_News_Content_Filter::filter_articles( $articles );
|
||||||
|
}
|
||||||
|
$filtered_out = $pre_filter_count - count( $articles );
|
||||||
|
|
||||||
|
// Store articles (deduplication handled by hash).
|
||||||
|
$stored = 0;
|
||||||
|
foreach ( $articles as $article ) {
|
||||||
|
if ( $this->store_article( $article ) ) {
|
||||||
|
$stored++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the fetch result.
|
||||||
|
update_option( 'peptide_news_last_fetch', array(
|
||||||
|
'time' => current_time( 'mysql' ),
|
||||||
|
'found' => $pre_filter_count,
|
||||||
|
'filtered_out' => $filtered_out,
|
||||||
|
'new_stored' => $stored,
|
||||||
|
) );
|
||||||
|
|
||||||
|
Peptide_News_Logger::info( sprintf(
|
||||||
|
'Fetch complete: %d found, %d filtered out, %d new stored.',
|
||||||
|
$pre_filter_count, $filtered_out, $stored
|
||||||
|
), 'fetch' );
|
||||||
|
|
||||||
|
// Run AI analysis on newly fetched articles (keywords + summary).
|
||||||
|
if ( class_exists( 'Peptide_News_LLM' ) && Peptide_News_LLM::is_enabled() ) {
|
||||||
|
$llm_batch_size = absint( get_option( 'peptide_news_llm_max_articles', 10 ) );
|
||||||
|
$llm_processed = Peptide_News_LLM::process_unanalyzed( $llm_batch_size );
|
||||||
|
|
||||||
|
// Append LLM stats to the fetch log.
|
||||||
|
$fetch_log = get_option( 'peptide_news_last_fetch' );
|
||||||
|
if ( is_array( $fetch_log ) ) {
|
||||||
|
$fetch_log['ai_processed'] = $llm_processed;
|
||||||
|
update_option( 'peptide_news_last_fetch', $fetch_log );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune old articles beyond retention period.
|
||||||
|
$this->prune_old_articles();
|
||||||
|
|
||||||
|
// Release the fetch lock.
|
||||||
|
delete_transient( $lock_key );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch articles from configured RSS feeds.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_rss_feeds() {
|
||||||
|
$feeds_raw = get_option( 'peptide_news_rss_feeds', '' );
|
||||||
|
$feeds = array_filter( array_map( 'trim', explode( "\n", $feeds_raw ) ) );
|
||||||
|
$articles = array();
|
||||||
|
|
||||||
|
if ( ! function_exists( 'fetch_feed' ) ) {
|
||||||
|
require_once ABSPATH . WPINC . '/feed.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $feeds as $feed_url ) {
|
||||||
|
$feed = fetch_feed( esc_url_raw( $feed_url ) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $feed ) ) {
|
||||||
|
$this->log_error( 'RSS fetch failed for ' . $feed_url . ': ' . $feed->get_error_message() );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$max_items = $feed->get_item_quantity( 50 );
|
||||||
|
$items = $feed->get_items( 0, $max_items );
|
||||||
|
|
||||||
|
foreach ( $items as $item ) {
|
||||||
|
$pub_date = $item->get_date( 'Y-m-d H:i:s' );
|
||||||
|
if ( empty( $pub_date ) ) {
|
||||||
|
$pub_date = current_time( 'mysql' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$article_url = esc_url_raw( $item->get_permalink() );
|
||||||
|
$source_name = $this->resolve_article_source( $item, $feed_url, $article_url );
|
||||||
|
|
||||||
|
$articles[] = array(
|
||||||
|
'source' => $source_name,
|
||||||
|
'source_url' => $article_url,
|
||||||
|
'title' => sanitize_text_field( $item->get_title() ),
|
||||||
|
'excerpt' => wp_trim_words( wp_strip_all_tags( $item->get_description() ), 40 ),
|
||||||
|
'content' => wp_kses_post( $item->get_content() ),
|
||||||
|
'author' => sanitize_text_field( $item->get_author() ? $item->get_author()->get_name() : '' ),
|
||||||
|
'thumbnail_url' => '',
|
||||||
|
'published_at' => $pub_date,
|
||||||
|
'categories' => $this->extract_categories( $item ),
|
||||||
|
'tags' => '',
|
||||||
|
'language' => 'en',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch articles from NewsAPI.org.
|
||||||
|
*
|
||||||
|
* @param string $api_key
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_newsapi( $api_key ) {
|
||||||
|
$keywords = get_option( 'peptide_news_search_keywords', 'peptide research' );
|
||||||
|
$articles = array();
|
||||||
|
|
||||||
|
$url = add_query_arg( array(
|
||||||
|
'q' => urlencode( $keywords ),
|
||||||
|
'language' => 'en',
|
||||||
|
'sortBy' => 'publishedAt',
|
||||||
|
'pageSize' => 50,
|
||||||
|
'apiKey' => $api_key,
|
||||||
|
), 'https://newsapi.org/v2/everything' );
|
||||||
|
|
||||||
|
$response = wp_remote_get( $url, array(
|
||||||
|
'timeout' => 30,
|
||||||
|
'headers' => array( 'User-Agent' => 'PeptideNewsWP/' . PEPTIDE_NEWS_VERSION ),
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
$this->log_error( 'NewsAPI fetch failed: ' . $response->get_error_message() );
|
||||||
|
return $articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
|
||||||
|
if ( empty( $body['articles'] ) || 'ok' !== ( $body['status'] ?? '' ) ) {
|
||||||
|
$this->log_error( 'NewsAPI returned no articles or error status.' );
|
||||||
|
return $articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $body['articles'] as $item ) {
|
||||||
|
$pub_date = isset( $item['publishedAt'] )
|
||||||
|
? gmdate( 'Y-m-d H:i:s', strtotime( $item['publishedAt'] ) )
|
||||||
|
: current_time( 'mysql' );
|
||||||
|
|
||||||
|
$articles[] = array(
|
||||||
|
'source' => sanitize_text_field( $item['source']['name'] ?? 'NewsAPI' ),
|
||||||
|
'source_url' => esc_url_raw( $item['url'] ?? '' ),
|
||||||
|
'title' => sanitize_text_field( $item['title'] ?? '' ),
|
||||||
|
'excerpt' => wp_trim_words( sanitize_text_field( $item['description'] ?? '' ), 40 ),
|
||||||
|
'content' => wp_kses_post( $item['content'] ?? '' ),
|
||||||
|
'author' => sanitize_text_field( $item['author'] ?? '' ),
|
||||||
|
'thumbnail_url' => '',
|
||||||
|
'published_at' => $pub_date,
|
||||||
|
'categories' => '',
|
||||||
|
'tags' => '',
|
||||||
|
'language' => 'en',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store an article in the database. Deduplication via SHA-256 hash of URL.
|
||||||
|
*
|
||||||
|
* @param array $article
|
||||||
|
* @return bool True if new article was inserted.
|
||||||
|
*/
|
||||||
|
private function store_article( $article ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// Fetch from NewsAP
|
$table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
|
$hash = hash( 'sha256', $article['source_url'] );
|
||||||
|
|
||||||
|
// Check for duplicate.
|
||||||
|
$exists = $wpdb->get_var( $wpdb->prepare(
|
||||||
|
"SELECT id FROM {$table} WHERE hash = %s",
|
||||||
|
$hash
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( $exists ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$table,
|
||||||
|
array(
|
||||||
|
'source' => $article['source'],
|
||||||
|
'source_url' => $article['source_url'],
|
||||||
|
'title' => $article['title'],
|
||||||
|
'excerpt' => $article['excerpt'],
|
||||||
|
'content' => $article['content'],
|
||||||
|
'author' => $article['author'],
|
||||||
|
'thumbnail_url' => $article['thumbnail_url'],
|
||||||
|
'published_at' => $article['published_at'],
|
||||||
|
'fetched_at' => current_time( 'mysql' ),
|
||||||
|
'categories' => $article['categories'],
|
||||||
|
'tags' => $article['tags'],
|
||||||
|
'language' => $article['language'],
|
||||||
|
'hash' => $hash,
|
||||||
|
'is_active' => 1,
|
||||||
|
),
|
||||||
|
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d' )
|
||||||
|
);
|
||||||
|
|
||||||
|
return false !== $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove articles older than the retention period.
|
||||||
|
*/
|
||||||
|
private function prune_old_articles() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$retention_days = (int) get_option( 'peptide_news_article_retention', 90 );
|
||||||
|
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
|
||||||
|
|
||||||
|
$table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"UPDATE {$table} SET is_active = 0 WHERE published_at < %s",
|
||||||
|
$cutoff
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all article transient caches.
|
||||||
|
*/
|
||||||
|
private function clear_article_cache() {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options}
|
||||||
|
WHERE option_name LIKE %s
|
||||||
|
OR option_name LIKE %s",
|
||||||
|
$wpdb->esc_like( '_transient_peptide_news_articles_' ) . '%',
|
||||||
|
$wpdb->esc_like( '_transient_timeout_peptide_news_articles_' ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract categories from a SimplePie item.
|
||||||
|
*
|
||||||
|
* @param SimplePie_Item $item
|
||||||
|
* @return string Comma-separated categories.
|
||||||
|
*/
|
||||||
|
private function extract_categories( $item ) {
|
||||||
|
$categories = $item->get_categories();
|
||||||
|
if ( empty( $categories ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$names = array();
|
||||||
|
foreach ( $categories as $cat ) {
|
||||||
|
$names[] = sanitize_text_field( $cat->get_label() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode( ', ', array_filter( $names ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the real source name for an RSS feed item.
|
||||||
|
*
|
||||||
|
* Google News RSS wraps articles from other publishers behind
|
||||||
|
* news.google.com URLs, so the feed-level domain is useless.
|
||||||
|
* This method tries several strategies in priority order:
|
||||||
|
*
|
||||||
|
* 1. SimplePie <source> element (Google News RSS includes this).
|
||||||
|
* 2. Title suffix — Google News formats titles as "Headline - Source".
|
||||||
|
* 3. Resolve the redirect URL to get the actual publisher domain.
|
||||||
|
* 4. Fall back to the article permalink domain.
|
||||||
|
* 5. Last resort: feed URL domain.
|
||||||
|
*
|
||||||
|
* @param SimplePie_Item $item The RSS item.
|
||||||
|
* @param string $feed_url The feed URL.
|
||||||
|
* @param string $article_url The article permalink.
|
||||||
|
* @return string Source name or domain.
|
||||||
|
*/
|
||||||
|
private function resolve_article_source( $item, $feed_url, $article_url ) {
|
||||||
|
$feed_domain = $this->extract_domain( $feed_url );
|
||||||
|
$is_aggregator = in_array(
|
||||||
|
$feed_domain,
|
||||||
|
array( 'news.google.com', 'news.yahoo.com', 'msn.com', 'feedly.com' ),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Strategy 1: SimplePie <source> element.
|
||||||
|
$source_obj = $item->get_source();
|
||||||
|
if ( $source_obj ) {
|
||||||
|
$source_title = $source_obj->get_title();
|
||||||
|
if ( ! empty( $source_title ) ) {
|
||||||
|
return sanitize_text_field( $source_title );
|
||||||
|
}
|
||||||
|
$source_link = $source_obj->get_link();
|
||||||
|
if ( ! empty( $source_link ) ) {
|
||||||
|
$domain = $this->extract_domain( $source_link );
|
||||||
|
if ( 'unknown' !== $domain ) {
|
||||||
|
return $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Parse "Headline - Source" from title (common in aggregator feeds).
|
||||||
|
if ( $is_aggregator ) {
|
||||||
|
$title = $item->get_title();
|
||||||
|
if ( ! empty( $title ) && preg_match( '/\s[-\x{2013}\x{2014}]\s([^-\x{2013}\x{2014}]+)$/u', $title, $matches ) ) {
|
||||||
|
$candidate = trim( $matches[1] );
|
||||||
|
if ( mb_strlen( $candidate ) <= 60 && mb_strlen( $candidate ) >= 2 ) {
|
||||||
|
return sanitize_text_field( $candidate );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Resolve Google News redirect URL to actual destination.
|
||||||
|
if ( $is_aggregator ) {
|
||||||
|
$resolved = $this->resolve_redirect_url( $article_url );
|
||||||
|
if ( $resolved && $resolved !== $article_url ) {
|
||||||
|
$domain = $this->extract_domain( $resolved );
|
||||||
|
if ( 'unknown' !== $domain && $domain !== $feed_domain ) {
|
||||||
|
return $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 4: Use the article permalink domain.
|
||||||
|
$article_domain = $this->extract_domain( $article_url );
|
||||||
|
if ( 'unknown' !== $article_domain && $article_domain !== $feed_domain ) {
|
||||||
|
return $article_domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 5: Fall back to the feed domain.
|
||||||
|
return $feed_domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a redirect URL to its final destination without downloading the body.
|
||||||
|
*
|
||||||
|
* @param string $url The URL to resolve.
|
||||||
|
* @return string|false The final URL or false on failure.
|
||||||
|
*/
|
||||||
|
private function resolve_redirect_url( $url ) {
|
||||||
|
if ( empty( $url ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_head( $url, array(
|
||||||
|
'timeout' => 8,
|
||||||
|
'redirection' => 5,
|
||||||
|
'user-agent' => 'Mozilla/5.0 (compatible; PeptideNewsBot/1.0)',
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$final_url = wp_remote_retrieve_header( $response, 'location' );
|
||||||
|
|
||||||
|
if ( empty( $final_url ) && isset( $response['http_response'] ) ) {
|
||||||
|
$http_response = $response['http_response'];
|
||||||
|
if ( method_exists( $http_response, 'get_response_object' ) ) {
|
||||||
|
$raw = $http_response->get_response_object();
|
||||||
|
if ( isset( $raw->url ) ) {
|
||||||
|
return $raw->url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! empty( $final_url ) ? $final_url : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill source names for existing articles that show an aggregator domain.
|
||||||
|
*
|
||||||
|
* Parses the " - Source" suffix from stored titles to update the source column.
|
||||||
|
*
|
||||||
|
* @return int Number of articles updated.
|
||||||
|
*/
|
||||||
|
public function backfill_article_sources() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
|
$aggregators = array( 'news.google.com', 'news.yahoo.com', 'msn.com' );
|
||||||
|
$placeholders = implode( ', ', array_fill( 0, count( $aggregators ), '%s' ) );
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||||
|
$articles = $wpdb->get_results( $wpdb->prepare(
|
||||||
|
"SELECT id, title, source, source_url
|
||||||
|
FROM {$table}
|
||||||
|
WHERE source IN ({$placeholders})
|
||||||
|
AND is_active = 1",
|
||||||
|
...$aggregators
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( empty( $articles ) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = 0;
|
||||||
|
|
||||||
|
foreach ( $articles as $article ) {
|
||||||
|
$new_source = '';
|
||||||
|
|
||||||
|
// Try parsing "Headline - Source" from title.
|
||||||
|
if ( preg_match( '/\s[-\x{2013}\x{2014}]\s([^-\x{2013}\x{2014}]+)$/u', $article->title, $matches ) ) {
|
||||||
|
$candidate = trim( $matches[1] );
|
||||||
|
if ( mb_strlen( $candidate ) <= 60 && mb_strlen( $candidate ) >= 2 ) {
|
||||||
|
$new_source = sanitize_text_field( $candidate );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to resolving the redirect URL.
|
||||||
|
if ( empty( $new_source ) ) {
|
||||||
|
$resolved = $this->resolve_redirect_url( $article->source_url );
|
||||||
|
if ( $resolved && $resolved !== $article->source_url ) {
|
||||||
|
$domain = $this->extract_domain( $resolved );
|
||||||
|
if ( 'unknown' !== $domain && ! in_array( $domain, $aggregators, true ) ) {
|
||||||
|
$new_source = $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usleep( 300000 ); // 0.3s throttle for HTTP requests.
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $new_source ) && $new_source !== $article->source ) {
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array( 'source' => $new_source ),
|
||||||
|
array( 'id' => $article->id ),
|
||||||
|
array( '%s' ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $updated > 0 ) {
|
||||||
|
$this->clear_article_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for the backfill action.
|
||||||
|
*/
|
||||||
|
public function ajax_backfill_sources() {
|
||||||
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( 'Unauthorized', 403 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = $this->backfill_article_sources();
|
||||||
|
wp_send_json_success( array( 'updated' => $updated ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract domain from a URL for source identification.
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function extract_domain( $url ) {
|
||||||
|
$host = wp_parse_url( $url, PHP_URL_HOST );
|
||||||
|
return $host ? preg_replace( '/^www\./', '', $host ) : 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an error to the WordPress debug log.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
*/
|
||||||
|
private function log_error( $message ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
error_log( '[Peptide News] ' . $message );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,14 +75,15 @@ class Peptide_News_LLM {
|
|||||||
if ( ! is_wp_error( $keywords_result ) ) {
|
if ( ! is_wp_error( $keywords_result ) ) {
|
||||||
$results['keywords'] = self::sanitize_keywords( $keywords_result );
|
$results['keywords'] = self::sanitize_keywords( $keywords_result );
|
||||||
$any_success = true;
|
$any_success = true;
|
||||||
|
Peptide_News_Logger::debug( 'Keywords extracted for article #' . $article->id, 'llm' );
|
||||||
} else {
|
} else {
|
||||||
$err_msg = 'Keywords (' . $keywords_model . '): ' . $keywords_result->get_error_message();
|
$err_msg = 'Keywords (' . $keywords_model . '): ' . $keywords_result->get_error_message();
|
||||||
$results['errors'][] = $err_msg;
|
$results['errors'][] = $err_msg;
|
||||||
self::log_error( 'Keyword extraction failed for article ' . $article->id . ': ' . $keywords_result->get_error_message() );
|
Peptide_News_Logger::error( 'Keyword extraction failed for article #' . $article->id . ': ' . $keywords_result->get_error_message(), 'llm' );
|
||||||
}
|
}
|
||||||
} elseif ( $need_keywords ) {
|
} elseif ( $need_keywords ) {
|
||||||
$results['errors'][] = 'Invalid keywords model ID: ' . $keywords_model;
|
$results['errors'][] = 'Invalid keywords model ID: ' . $keywords_model;
|
||||||
self::log_error( 'Invalid keywords model ID: ' . $keywords_model );
|
Peptide_News_Logger::error( 'Invalid keywords model ID: ' . $keywords_model, 'llm' );
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Summarization ---
|
// --- Summarization ---
|
||||||
@@ -97,14 +98,15 @@ class Peptide_News_LLM {
|
|||||||
// Strip all HTML tags from LLM output to prevent XSS.
|
// Strip all HTML tags from LLM output to prevent XSS.
|
||||||
$results['summary'] = sanitize_textarea_field( wp_strip_all_tags( $summary_result ) );
|
$results['summary'] = sanitize_textarea_field( wp_strip_all_tags( $summary_result ) );
|
||||||
$any_success = true;
|
$any_success = true;
|
||||||
|
Peptide_News_Logger::info( 'AI summary generated for article #' . $article->id . ': ' . mb_substr( $article->title, 0, 60 ), 'llm' );
|
||||||
} else {
|
} else {
|
||||||
$err_msg = 'Summary (' . $summary_model . '): ' . $summary_result->get_error_message();
|
$err_msg = 'Summary (' . $summary_model . '): ' . $summary_result->get_error_message();
|
||||||
$results['errors'][] = $err_msg;
|
$results['errors'][] = $err_msg;
|
||||||
self::log_error( 'Summarization failed for article ' . $article->id . ': ' . $summary_result->get_error_message() );
|
Peptide_News_Logger::error( 'Summarization failed for article #' . $article->id . ': ' . $summary_result->get_error_message(), 'llm' );
|
||||||
}
|
}
|
||||||
} elseif ( $need_summary ) {
|
} elseif ( $need_summary ) {
|
||||||
$results['errors'][] = 'Invalid summary model ID: ' . $summary_model;
|
$results['errors'][] = 'Invalid summary model ID: ' . $summary_model;
|
||||||
self::log_error( 'Invalid summary model ID: ' . $summary_model );
|
Peptide_News_Logger::error( 'Invalid summary model ID: ' . $summary_model, 'llm' );
|
||||||
}
|
}
|
||||||
|
|
||||||
$results['success'] = $any_success;
|
$results['success'] = $any_success;
|
||||||
@@ -198,6 +200,12 @@ class Peptide_News_LLM {
|
|||||||
'errors' => $last_errors,
|
'errors' => $last_errors,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
|
if ( $processed > 0 ) {
|
||||||
|
Peptide_News_Logger::info( sprintf( 'AI batch complete: %d/%d articles processed.', $processed, count( $articles ) ), 'llm' );
|
||||||
|
} elseif ( count( $articles ) > 0 ) {
|
||||||
|
Peptide_News_Logger::warning( sprintf( 'AI batch: 0/%d articles succeeded.', count( $articles ) ), 'llm' );
|
||||||
|
}
|
||||||
|
|
||||||
return $processed;
|
return $processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +415,8 @@ class Peptide_News_LLM {
|
|||||||
wp_send_json_error( 'AI Analysis is not enabled. Please enable it and set an OpenRouter API key in Peptide News → Settings.' );
|
wp_send_json_error( 'AI Analysis is not enabled. Please enable it and set an OpenRouter API key in Peptide News → Settings.' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Peptide_News_Logger::info( 'Bulk AI summary generation triggered by admin.', 'llm' );
|
||||||
|
|
||||||
// Auto-correct known broken model names to a working free model.
|
// Auto-correct known broken model names to a working free model.
|
||||||
$broken_models = array(
|
$broken_models = array(
|
||||||
'qwen/qwen3.6-plus:free',
|
'qwen/qwen3.6-plus:free',
|
||||||
|
|||||||
254
peptide-news-plugin/includes/class-peptide-news-logger.php
Normal file
254
peptide-news-plugin/includes/class-peptide-news-logger.php
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Logger.
|
||||||
|
*
|
||||||
|
* Provides structured logging to a custom database table with levels,
|
||||||
|
* contexts, and an admin-facing viewer with AJAX pagination and clear.
|
||||||
|
*
|
||||||
|
* @since 2.1.0
|
||||||
|
*/
|
||||||
|
class Peptide_News_Logger {
|
||||||
|
|
||||||
|
/** Log level constants. */
|
||||||
|
const LEVEL_INFO = 'info';
|
||||||
|
const LEVEL_WARNING = 'warning';
|
||||||
|
const LEVEL_ERROR = 'error';
|
||||||
|
const LEVEL_DEBUG = 'debug';
|
||||||
|
|
||||||
|
/** Maximum rows kept in the log table (auto-pruned on write). */
|
||||||
|
const MAX_ROWS = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the log table name.
|
||||||
|
*/
|
||||||
|
public static function table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . 'peptide_news_log';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Writing
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a log entry.
|
||||||
|
*
|
||||||
|
* @param string $level One of the LEVEL_* constants.
|
||||||
|
* @param string $message Human-readable message.
|
||||||
|
* @param string $context Category tag (fetch, llm, admin, cron, general).
|
||||||
|
*/
|
||||||
|
public static function log( $level, $message, $context = 'general' ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
self::table(),
|
||||||
|
array(
|
||||||
|
'level' => $level,
|
||||||
|
'context' => $context,
|
||||||
|
'message' => $message,
|
||||||
|
'created_at' => current_time( 'mysql' ),
|
||||||
|
),
|
||||||
|
array( '%s', '%s', '%s', '%s' )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-prune old entries.
|
||||||
|
self::maybe_prune();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: info level. */
|
||||||
|
public static function info( $message, $context = 'general' ) {
|
||||||
|
self::log( self::LEVEL_INFO, $message, $context );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: warning level. */
|
||||||
|
public static function warning( $message, $context = 'general' ) {
|
||||||
|
self::log( self::LEVEL_WARNING, $message, $context );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: error level. */
|
||||||
|
public static function error( $message, $context = 'general' ) {
|
||||||
|
self::log( self::LEVEL_ERROR, $message, $context );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: debug level (only writes when WP_DEBUG is on). */
|
||||||
|
public static function debug( $message, $context = 'general' ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
self::log( self::LEVEL_DEBUG, $message, $context );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Reading
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve log entries.
|
||||||
|
*
|
||||||
|
* @param array $args {
|
||||||
|
* Optional. Query arguments.
|
||||||
|
*
|
||||||
|
* @type string $level Filter by level.
|
||||||
|
* @type string $context Filter by context.
|
||||||
|
* @type int $limit Number of rows.
|
||||||
|
* @type int $offset Pagination offset.
|
||||||
|
* }
|
||||||
|
* @return array { 'rows' => array, 'total' => int }
|
||||||
|
*/
|
||||||
|
public static function get_logs( $args = array() ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$defaults = array(
|
||||||
|
'level' => '',
|
||||||
|
'context' => '',
|
||||||
|
'limit' => 50,
|
||||||
|
'offset' => 0,
|
||||||
|
);
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
|
||||||
|
$table = self::table();
|
||||||
|
$where = array( '1=1' );
|
||||||
|
$vals = array();
|
||||||
|
|
||||||
|
if ( ! empty( $args['level'] ) ) {
|
||||||
|
$where[] = 'level = %s';
|
||||||
|
$vals[] = $args['level'];
|
||||||
|
}
|
||||||
|
if ( ! empty( $args['context'] ) ) {
|
||||||
|
$where[] = 'context = %s';
|
||||||
|
$vals[] = $args['context'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_sql = implode( ' AND ', $where );
|
||||||
|
|
||||||
|
// Total count.
|
||||||
|
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||||
|
$total = empty( $vals )
|
||||||
|
? (int) $wpdb->get_var( $count_sql )
|
||||||
|
: (int) $wpdb->get_var( $wpdb->prepare( $count_sql, $vals ) );
|
||||||
|
|
||||||
|
// Rows.
|
||||||
|
$order_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d";
|
||||||
|
$vals[] = absint( $args['limit'] );
|
||||||
|
$vals[] = absint( $args['offset'] );
|
||||||
|
$rows = $wpdb->get_results( $wpdb->prepare( $order_sql, $vals ) );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'rows' => $rows ? $rows : array(),
|
||||||
|
'total' => $total,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Maintenance
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove rows beyond the MAX_ROWS cap.
|
||||||
|
*/
|
||||||
|
private static function maybe_prune() {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::table();
|
||||||
|
|
||||||
|
// Only prune every ~50 writes (probabilistic).
|
||||||
|
if ( wp_rand( 1, 50 ) !== 1 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
|
||||||
|
|
||||||
|
if ( $count > self::MAX_ROWS ) {
|
||||||
|
$delete_count = $count - self::MAX_ROWS;
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"DELETE FROM {$table} ORDER BY id ASC LIMIT %d",
|
||||||
|
$delete_count
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all log entries.
|
||||||
|
*/
|
||||||
|
public static function clear() {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query( "TRUNCATE TABLE " . self::table() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* AJAX handlers
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: fetch paginated log entries.
|
||||||
|
*/
|
||||||
|
public static function ajax_get_logs() {
|
||||||
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( 'Unauthorized', 403 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
||||||
|
$level = isset( $_POST['level'] ) ? sanitize_text_field( $_POST['level'] ) : '';
|
||||||
|
$context = isset( $_POST['context'] ) ? sanitize_text_field( $_POST['context'] ) : '';
|
||||||
|
$per_page = 50;
|
||||||
|
|
||||||
|
$result = self::get_logs( array(
|
||||||
|
'level' => $level,
|
||||||
|
'context' => $context,
|
||||||
|
'limit' => $per_page,
|
||||||
|
'offset' => ( $page - 1 ) * $per_page,
|
||||||
|
) );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'rows' => $result['rows'],
|
||||||
|
'total' => $result['total'],
|
||||||
|
'page' => $page,
|
||||||
|
'total_pages' => ceil( $result['total'] / $per_page ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: clear all log entries.
|
||||||
|
*/
|
||||||
|
public static function ajax_clear_logs() {
|
||||||
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( 'Unauthorized', 403 );
|
||||||
|
}
|
||||||
|
|
||||||
|
self::clear();
|
||||||
|
self::info( 'Log cleared by admin.', 'admin' );
|
||||||
|
|
||||||
|
wp_send_json_success( array( 'message' => 'Log cleared.' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Schema
|
||||||
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the log table (called from activator / upgrade check).
|
||||||
|
*/
|
||||||
|
public static function create_table() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = self::table();
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE {$table} (
|
||||||
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
level VARCHAR(10) NOT NULL DEFAULT 'info',
|
||||||
|
context VARCHAR(30) NOT NULL DEFAULT 'general',
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_level (level),
|
||||||
|
KEY idx_context (context),
|
||||||
|
KEY idx_created (created_at)
|
||||||
|
) {$charset_collate};";
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
dbDelta( $sql );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ class Peptide_News {
|
|||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-api.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-api.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-llm.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-llm.php';
|
||||||
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-logger.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-content-filter.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-content-filter.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'admin/class-peptide-news-admin.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'admin/class-peptide-news-admin.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'public/class-peptide-news-public.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'public/class-peptide-news-public.php';
|
||||||
@@ -82,6 +83,8 @@ class Peptide_News {
|
|||||||
$this->loader->add_filter( 'cron_schedules', $fetcher, 'add_custom_cron_schedules' );
|
$this->loader->add_filter( 'cron_schedules', $fetcher, 'add_custom_cron_schedules' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_backfill_sources', $fetcher, 'ajax_backfill_sources' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_backfill_sources', $fetcher, 'ajax_backfill_sources' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_generate_summaries', 'Peptide_News_LLM', 'ajax_generate_summaries' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_generate_summaries', 'Peptide_News_LLM', 'ajax_generate_summaries' );
|
||||||
|
$this->loader->add_action( 'wp_ajax_peptide_news_get_logs', 'Peptide_News_Logger', 'ajax_get_logs' );
|
||||||
|
$this->loader->add_action( 'wp_ajax_peptide_news_clear_logs', 'Peptide_News_Logger', 'ajax_clear_logs' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Peptide News Aggregator
|
* Plugin Name: Peptide News Aggregator
|
||||||
* Plugin URI: https://github.com/peptiderepo/peptide-news-plugin
|
* Plugin URI: https://github.com/peptiderepo/peptide-news-plugin
|
||||||
* Description: Aggregates and displays the latest peptide research news from multiple sources with click analytics and trend reporting.
|
* Description: Aggregates and displays the latest peptide research news from multiple sources with click analytics and trend reporting.
|
||||||
* Version: 2.0.1
|
* Version: 2.1.0
|
||||||
* Author: Peptide News Team
|
* Author: Peptide News Team
|
||||||
* Author URI: https://github.com/peptiderepo
|
* Author URI: https://github.com/peptiderepo
|
||||||
* License: GPL-2.0+
|
* License: GPL-2.0+
|
||||||
@@ -22,7 +22,7 @@ if ( ! defined( 'WPINC' ) ) {
|
|||||||
/**
|
/**
|
||||||
* Current plugin version — follows SemVer.
|
* Current plugin version — follows SemVer.
|
||||||
*/
|
*/
|
||||||
define( 'PEPTIDE_NEWS_VERSION', '2.0.1' );
|
define( 'PEPTIDE_NEWS_VERSION', '2.1.0' );
|
||||||
define( 'PEPTIDE_NEWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
define( 'PEPTIDE_NEWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'PEPTIDE_NEWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
define( 'PEPTIDE_NEWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
define( 'PEPTIDE_NEWS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
define( 'PEPTIDE_NEWS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||||
@@ -60,6 +60,7 @@ function peptide_news_check_db_upgrade() {
|
|||||||
$installed_version = get_option( 'peptide_news_db_version', '1.0.0' );
|
$installed_version = get_option( 'peptide_news_db_version', '1.0.0' );
|
||||||
if ( version_compare( $installed_version, PEPTIDE_NEWS_VERSION, '<' ) ) {
|
if ( version_compare( $installed_version, PEPTIDE_NEWS_VERSION, '<' ) ) {
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-activator.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-activator.php';
|
||||||
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-logger.php';
|
||||||
Peptide_News_Activator::activate();
|
Peptide_News_Activator::activate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user