PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.25
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.25
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-site-health.php

class-metasync-site-health.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.25, at includes/class-metasync-site-health.php

1,353 lines 42.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * WordPress Site Health Integration for MetaSync
9 *
10 * Provides comprehensive WordPress cron queue health monitoring across all plugins,
11 * themes, and core functionality. Helps administrators identify queue overflow issues
12 * from any source while showing MetaSync's contribution.
13 *
14 * @link https://searchatlas.com
15 * @since 1.0.0
16 *
17 * @package Metasync
18 * @subpackage Metasync/includes
19 */
20
21 /**
22 * Site Health integration class.
23 *
24 * Monitors the entire WordPress cron system and provides detailed statistics
25 * about pending jobs, recurring events, and plugin-specific contributions.
26 *
27 * @since 1.0.0
28 * @package Metasync
29 * @subpackage Metasync/includes
30 * @author Engineering Team <[email protected]>
31 */
32 class Metasync_Site_Health
33 {
34 /**
35 * Maximum safe cron option size (3MB)
36 * Above this size, we won't attempt to load the cron array
37 */
38 const MAX_SAFE_CRON_SIZE = 3145728; // 3MB
39
40 /**
41 * Maximum jobs to count before early exit
42 * Prevents excessive processing on sites with huge queues
43 */
44 const MAX_COUNT_THRESHOLD = 5000;
45
46 /**
47 * Threshold for recommended status warning
48 * One-time pending jobs above this trigger a warning
49 */
50 const PENDING_JOBS_THRESHOLD = 1000;
51
52 /**
53 * Memory usage threshold for critical status (90%)
54 * Memory usage above this percentage triggers critical warning
55 */
56 const MEMORY_CRITICAL_THRESHOLD = 90;
57
58 /**
59 * OTTO API rate limit hits in 24h that trigger a recommended warning
60 */
61 const RATE_LIMIT_WARNING_THRESHOLD = 5;
62
63 /**
64 * OTTO API rate limit hits in 24h that trigger a critical warning
65 */
66 const RATE_LIMIT_CRITICAL_THRESHOLD = 20;
67
68 /**
69 * Failed MetaSync cron actions in 24h that trigger a recommended warning
70 */
71 const FAILED_ACTIONS_THRESHOLD = 100;
72
73 /**
74 * Initialize the Site Health integration
75 *
76 * @since 1.0.0
77 */
78 public function __construct()
79 {
80 // Register debug information for Site Health Info tab
81 $this->register_debug_information();
82 }
83
84 /**
85 * Register MetaSync debug information for the Site Health Info tab
86 *
87 * @since 1.0.0
88 */
89 private function register_debug_information()
90 {
91 add_filter( 'debug_information', [ $this, 'add_debug_info' ] );
92 }
93
94 /**
95 * Build an HTML anchor tag pointing to a MetaSync admin page.
96 *
97 * Uses the whitelabel-aware static page slug from Metasync_Admin so that
98 * links remain correct on white-labelled installs.
99 *
100 * @since 1.0.0
101 * @param string $path URL fragment appended after the page slug (e.g. '&tab=general' or '-sync-log').
102 * Use an empty string to link to the plugin root page.
103 * @param string $label Visible link text shown to the administrator.
104 * @return string HTML anchor tag, ready to embed in a description string.
105 */
106 private function get_admin_link( string $path, string $label ): string
107 {
108 $url = admin_url( 'admin.php?page=' . Metasync_Admin::$page_slug . $path );
109 return sprintf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( $label ) );
110 }
111
112 /**
113 * Add MetaSync debug information section to the Site Health Info tab
114 *
115 * Registers a debug information group with the whitelabeled plugin name.
116 *
117 * @since 1.0.0
118 * @param array $info Existing debug information
119 * @return array Updated debug information with MetaSync fields
120 */
121 public function add_debug_info( $info )
122 {
123 $plugin_name = Metasync::get_effective_plugin_name();
124
125 $queue_stats = $this->get_queue_stats();
126 $failed_stats = $this->get_failed_actions_stats();
127 $performance_fields = $this->get_performance_settings();
128
129 $sync_db = new Metasync_Sync_History_Database();
130 $sync_stats = $sync_db->get_statistics();
131 $completed_count = isset( $sync_stats->published_count ) ? (int) $sync_stats->published_count : 0;
132
133 $memory_stats = $this->get_memory_stats();
134 $unlimited = $memory_stats['limit_bytes'] === -1;
135
136 $rate_limit_log = get_option( 'metasync_otto_rate_limit_log', [] );
137 $last_backoff_time = ! empty( $rate_limit_log ) ? max( $rate_limit_log ) : null;
138 $last_backoff_value = $last_backoff_time
139 ? wp_date( 'Y-m-d H:i:s', $last_backoff_time ) . ' (' . human_time_diff( $last_backoff_time ) . ' ago)'
140 : __( 'Never' );
141
142 $otto_job_counts = class_exists( 'Metasync_Otto_Job_Status' ) ? Metasync_Otto_Job_Status::counts() : [];
143 $otto_job_fields = [];
144 if ( ! empty( $otto_job_counts ) ) {
145 $otto_job_fields = [
146 'otto_jobs_accepted' => [
147 'label' => __( 'Crawl Jobs - Accepted' ),
148 'value' => (int) $otto_job_counts['accepted'],
149 ],
150 'otto_jobs_retrying' => [
151 'label' => __( 'Crawl Jobs - Retrying' ),
152 'value' => (int) $otto_job_counts['retrying'],
153 ],
154 'otto_jobs_completed' => [
155 'label' => __( 'Crawl Jobs - Completed' ),
156 'value' => (int) $otto_job_counts['completed'],
157 ],
158 'otto_jobs_failed' => [
159 'label' => __( 'Crawl Jobs - Failed' ),
160 'value' => (int) $otto_job_counts['failed'],
161 ],
162 'otto_jobs_pending' => [
163 'label' => __( 'Crawl Jobs - Queued Overflow' ),
164 'value' => (int) $otto_job_counts['pending'],
165 ],
166 ];
167 }
168
169 $info['metasync'] = [
170 'label' => $plugin_name,
171 'fields' => [
172 'version' => [
173 'label' => __( 'Plugin Version' ),
174 'value' => METASYNC_VERSION,
175 ],
176 'queue_pending' => [
177 'label' => __( 'Queue - Pending Jobs' ),
178 'value' => isset( $queue_stats['metasync_pending'] ) ? (int) $queue_stats['metasync_pending'] : 0,
179 ],
180 'queue_completed' => [
181 'label' => __( 'Queue - Completed Jobs' ),
182 'value' => $completed_count,
183 ],
184 'queue_failed_24h' => [
185 'label' => __( 'Queue - Failed Jobs (last 24h)' ),
186 'value' => isset( $failed_stats['count'] ) ? (int) $failed_stats['count'] : 0,
187 ],
188 ] + $performance_fields + $otto_job_fields + [
189 'last_api_backoff' => [
190 'label' => __( 'Last API Backoff Event' ),
191 'value' => $last_backoff_value,
192 ],
193 'memory_limit' => [
194 'label' => __( 'Memory - PHP Limit' ),
195 'value' => $memory_stats['limit_formatted'],
196 ],
197 'memory_current' => [
198 'label' => __( 'Memory - Current Usage' ),
199 'value' => $memory_stats['current_formatted'],
200 ],
201 'memory_peak' => [
202 'label' => __( 'Memory - Peak Usage' ),
203 'value' => $memory_stats['peak_formatted'],
204 ],
205 'memory_percentage' => [
206 'label' => __( 'Memory - Usage Percentage' ),
207 'value' => $unlimited ? __( 'N/A (unlimited)' ) : $memory_stats['percentage'] . '%',
208 ],
209 'memory_available' => [
210 'label' => __( 'Memory - Available' ),
211 'value' => $memory_stats['available_formatted'],
212 ],
213 ],
214 ];
215
216 return $info;
217 }
218
219 /**
220 * Register Site Health tests with WordPress
221 *
222 * Hooks into the site_status_tests filter to add our custom tests.
223 * Uses 'direct' test type which runs immediately when the Site Health page is loaded.
224 *
225 * @since 1.0.0
226 */
227 public function register_tests()
228 {
229 add_filter('site_status_tests', function ($tests) {
230 // WordPress Cron Queue Health Check
231 $tests['direct']['metasync_queue_size'] = [
232 'label' => __('WordPress Cron Queue Health'),
233 'test' => [$this, 'queue_size_check']
234 ];
235
236 // Memory Usage Health Check
237 $tests['direct']['metasync_memory_usage'] = [
238 'label' => __('PHP Memory Usage'),
239 'test' => [$this, 'memory_usage_check']
240 ];
241
242 // OTTO API Rate Limit Health Check
243 $tests['direct']['metasync_otto_rate_limit'] = [
244 'label' => sprintf(__('%s OTTO API Rate Limit'), Metasync::get_effective_plugin_name()),
245 'test' => [$this, 'otto_rate_limit_check']
246 ];
247
248 // Debug Mode Health Check
249 $tests['direct']['metasync_debug_mode'] = [
250 'label' => __('WordPress Debug Mode'),
251 'test' => [$this, 'debug_mode_check']
252 ];
253
254 // Failed Actions Health Check
255 $tests['direct']['metasync_failed_actions'] = [
256 'label' => sprintf(__('%s Failed Actions'), Metasync::get_effective_plugin_name()),
257 'test' => [$this, 'failed_actions_check']
258 ];
259
260 // OTTO Crawl Job Reliability Health Check
261 $tests['direct']['metasync_otto_job_reliability'] = [
262 'label' => sprintf(__('%s Crawl Job Reliability'), Metasync::get_whitelabel_otto_name()),
263 'test' => [$this, 'otto_job_reliability_check']
264 ];
265
266 return $tests;
267 });
268 }
269
270 /**
271 * Get the size of the cron option in the database using direct SQL
272 *
273 * This method checks the byte size of the serialized cron data WITHOUT
274 * loading and unserializing it, providing excellent performance even
275 * on sites with bloated cron queues.
276 *
277 * @since 1.0.0
278 * @return int Size in bytes of the cron option_value
279 */
280 private function get_cron_option_size()
281 {
282 global $wpdb;
283
284 $size = $wpdb->get_var(
285 $wpdb->prepare(
286 "SELECT LENGTH(option_value) FROM {$wpdb->options} WHERE option_name = %s",
287 'cron'
288 )
289 );
290
291 return (int) $size;
292 }
293
294 /**
295 * Get comprehensive queue statistics with bifurcated breakdown
296 *
297 * Analyzes the entire WordPress cron system and categorizes jobs:
298 * - One-time events (true pending jobs that are removed after execution)
299 * - Recurring events (permanent schedule entries that never get removed)
300 * - MetaSync-specific contribution (subset of the above)
301 *
302 * Uses performance-safe approach with early size check and count limits.
303 *
304 * @since 1.0.0
305 * @return array Array containing statistics or error information
306 */
307 private function get_queue_stats()
308 {
309 // First check size for safety - don't load huge cron arrays
310 $size = $this->get_cron_option_size();
311
312 // If cron data is excessively large (>3MB), don't try to load it
313 if ($size > self::MAX_SAFE_CRON_SIZE) {
314 return [
315 'error' => 'excessive_size',
316 'size' => $size,
317 'one_time' => -1,
318 'recurring' => -1,
319 'metasync_pending' => -1
320 ];
321 }
322
323 // Safe to load the cron array now
324 $cron = _get_cron_array();
325
326 if (empty($cron)) {
327 return [
328 'one_time' => 0,
329 'recurring' => 0,
330 'metasync_pending' => 0
331 ];
332 }
333
334 $one_time = 0;
335 $recurring = 0;
336 $metasync_pending = 0;
337
338 // Loop through ALL WordPress cron jobs (core, plugins, themes, custom)
339 foreach ($cron as $hooks) {
340 foreach ($hooks as $hook => $events) { // ALL hooks from ALL sources
341 foreach ($events as $event) {
342 // Distinguish by schedule property
343 // schedule === false means one-time event (true pending job)
344 // schedule !== false means recurring event (permanent schedule)
345 if ($event['schedule'] === false) {
346 // One-time event (true pending job)
347 $one_time++;
348
349 // Track MetaSync's contribution separately
350 if (strpos($hook, 'metasync_') === 0) {
351 $metasync_pending++;
352 }
353 } else {
354 // Recurring event (permanent schedule entry)
355 $recurring++;
356 }
357
358 // Early exit for performance - stop counting after threshold
359 if ($one_time > self::MAX_COUNT_THRESHOLD) {
360 return [
361 'one_time' => self::MAX_COUNT_THRESHOLD,
362 'recurring' => $recurring,
363 'metasync_pending' => $metasync_pending,
364 'truncated' => true
365 ];
366 }
367 }
368 }
369 }
370
371 return [
372 'one_time' => $one_time,
373 'recurring' => $recurring,
374 'metasync_pending' => $metasync_pending
375 ];
376 }
377
378 /**
379 * Main Site Health test for WordPress cron queue
380 *
381 * Performs comprehensive health check of the entire WordPress cron system.
382 * Returns formatted result array for WordPress Site Health UI.
383 *
384 * Status levels:
385 * - good: <=1,000 pending jobs
386 * - critical: >1,000 pending jobs or cron data excessively large (>3MB)
387 *
388 * @since 1.0.0
389 * @return array Site Health test result
390 */
391 public function queue_size_check()
392 {
393 $stats = $this->get_queue_stats();
394
395 // Handle excessive size error
396 if (isset($stats['error']) && $stats['error'] === 'excessive_size') {
397 return [
398 'label' => __('WordPress cron queue is excessively large'),
399 'status' => 'critical',
400 'badge' => [
401 'label' => Metasync::get_effective_plugin_name(),
402 'color' => 'red'
403 ],
404 'description' => sprintf(
405 '<p>' . __('The WordPress cron data is excessively large (%s). This can cause severe performance issues including slow page loads, high memory usage, and potential timeouts.') . '</p>' .
406 '<p>' . __('This typically occurs when cron jobs accumulate from deactivated plugins or failed operations.') . '</p>',
407 size_format($stats['size'])
408 ),
409 'actions' => sprintf(
410 '<p><strong>%s</strong></p>' .
411 '<ul>' .
412 '<li>%s</li>' .
413 '<li>%s</li>' .
414 '<li>%s</li>' .
415 '</ul>',
416 __('Recommended Actions:'),
417 __('Use WP-CLI to list and clean up old cron jobs: <code>wp cron event list</code>'),
418 __('Install a cron management plugin to identify and remove orphaned jobs'),
419 __('Consider clearing the cron option and letting WordPress rebuild it from active plugins')
420 ),
421 'test' => 'metasync_queue_size'
422 ];
423 }
424
425 $one_time_count = $stats['one_time'];
426 $recurring_count = $stats['recurring'];
427 $metasync_count = $stats['metasync_pending'];
428 $truncated = isset($stats['truncated']) && $stats['truncated'];
429
430 // Determine status based on TOTAL system-wide ONE-TIME jobs from ALL sources
431 // This includes WordPress core, all plugins, themes, and custom code
432 if ($one_time_count <= self::PENDING_JOBS_THRESHOLD) {
433 $status = 'good';
434 $label = __('WordPress cron queue is healthy');
435 } else {
436 $status = 'critical';
437 $label = __('WordPress cron queue has high pending job count');
438 }
439
440 // Build detailed description showing system-wide statistics
441 $description = sprintf(
442 '<p><strong>%s</strong></p>' .
443 '<ul>' .
444 '<li>%s <strong>%s</strong></li>' .
445 '<li>%s %d</li>' .
446 '<li>%s %d</li>' .
447 '</ul>',
448 __('WordPress Cron Queue Statistics (All Sources):'),
449 __('Total pending jobs (one-time events):'),
450 $truncated ? sprintf(__('%d+'), $one_time_count) : number_format($one_time_count),
451 __('Total scheduled events (recurring):'),
452 $recurring_count,
453 sprintf(__('%s pending jobs:'), Metasync::get_effective_plugin_name()),
454 $metasync_count
455 );
456
457 // Add context about what these numbers mean
458 $description .= sprintf(
459 '<p><em>%s</em></p>',
460 __('Pending jobs are one-time tasks awaiting execution. Recurring events are permanent schedules that run automatically.')
461 );
462
463 // If status is not good, provide actionable recommendations
464 if ($status === 'recommended') {
465 $description .= sprintf(
466 '<p><strong>%s</strong></p>' .
467 '<ul>' .
468 '<li>%s</li>' .
469 '<li>%s</li>' .
470 '<li>%s</li>' .
471 '</ul>',
472 __('A high number of pending jobs may indicate:'),
473 __('WordPress cron is not running properly (check if WP-Cron is disabled or blocked)'),
474 __('High volume of scheduled tasks from plugins creating jobs faster than they can be processed'),
475 __('Slow server performance or resource constraints preventing timely job execution')
476 );
477
478 // If MetaSync is a significant contributor, add specific guidance
479 if ($metasync_count > 500) {
480 $description .= sprintf(
481 '<p><strong>%s</strong> %s</p>',
482 sprintf(__('%s Contribution:'), Metasync::get_effective_plugin_name()),
483 sprintf(
484 __('%1$s has %2$s pending %3$s SEO processing jobs. This may indicate high crawl volume from %3$s or slow processing. Consider reviewing %3$s crawl settings if this number continues to grow.'),
485 Metasync::get_effective_plugin_name(),
486 number_format($metasync_count),
487 Metasync::get_whitelabel_otto_name()
488 )
489 );
490 }
491
492 // Add helpful actions
493 $description .= sprintf(
494 '<p><strong>%s</strong></p>' .
495 '<ul>' .
496 '<li>%s</li>' .
497 '<li>%s</li>' .
498 '<li>%s</li>' .
499 '</ul>',
500 __('Recommended Actions:'),
501 __('Verify WordPress cron is functioning: Tools → Site Health → Info → WordPress Constants → DISABLE_WP_CRON should be false'),
502 __('Check server logs for cron execution errors or timeouts'),
503 __('Consider using a real system cron instead of WP-Cron for better reliability')
504 );
505
506 $description .= sprintf(
507 '<p>%s %s</p>',
508 __( 'Quick link:' ),
509 sprintf(
510 '<a href="%s">%s</a>',
511 esc_url( admin_url( 'site-health.php?tab=debug' ) ),
512 esc_html( __( 'Site Health Info (check DISABLE_WP_CRON)' ) )
513 )
514 );
515 }
516
517 return [
518 'label' => $label,
519 'status' => $status,
520 'badge' => [
521 'label' => Metasync::get_effective_plugin_name(),
522 'color' => $status === 'good' ? 'green' : 'red'
523 ],
524 'description' => $description,
525 'test' => 'metasync_queue_size'
526 ];
527 }
528
529 /**
530 * Convert PHP memory notation (e.g., "256M", "1G") to bytes
531 *
532 * @since 1.0.0
533 * @param string $value Memory value from PHP configuration
534 * @return int Memory value in bytes
535 */
536 private function convert_to_bytes($value)
537 {
538 $value = trim($value);
539 $last = strtolower($value[strlen($value) - 1]);
540 $value = (int) $value;
541
542 switch ($last) {
543 case 'g':
544 $value *= 1024;
545 // Fall through
546 case 'm':
547 $value *= 1024;
548 // Fall through
549 case 'k':
550 $value *= 1024;
551 break;
552 default:
553 // Value is already in bytes or has no unit suffix
554 break;
555 }
556
557 return $value;
558 }
559
560 /**
561 * Get comprehensive memory usage statistics
562 *
563 * Retrieves PHP memory configuration and current usage, calculating
564 * percentage and providing formatted human-readable values.
565 *
566 * @since 1.0.0
567 * @return array Array containing memory statistics
568 */
569 private function get_memory_stats()
570 {
571 // Get memory limit from PHP configuration
572 $memory_limit = ini_get('memory_limit');
573
574 // Handle unlimited memory (-1)
575 if ($memory_limit === '-1') {
576 return [
577 'limit' => -1,
578 'limit_bytes' => -1,
579 'limit_formatted' => __('Unlimited'),
580 'current' => memory_get_usage(true),
581 'current_formatted' => size_format(memory_get_usage(true)),
582 'peak' => memory_get_peak_usage(true),
583 'peak_formatted' => size_format(memory_get_peak_usage(true)),
584 'percentage' => 0,
585 'available' => -1,
586 'available_formatted' => __('Unlimited')
587 ];
588 }
589
590 // Convert limit to bytes
591 $limit_bytes = $this->convert_to_bytes($memory_limit);
592
593 // Get current memory usage
594 $current_usage = memory_get_usage(true);
595 $peak_usage = memory_get_peak_usage(true);
596
597 // Calculate percentage
598 $percentage = ($current_usage / $limit_bytes) * 100;
599
600 // Calculate available memory
601 $available = $limit_bytes - $current_usage;
602
603 return [
604 'limit' => $memory_limit,
605 'limit_bytes' => $limit_bytes,
606 'limit_formatted' => size_format($limit_bytes),
607 'current' => $current_usage,
608 'current_formatted' => size_format($current_usage),
609 'peak' => $peak_usage,
610 'peak_formatted' => size_format($peak_usage),
611 'percentage' => round($percentage, 2),
612 'available' => $available,
613 'available_formatted' => size_format($available)
614 ];
615 }
616
617 /**
618 * Main Site Health test for PHP memory usage
619 *
620 * Monitors PHP memory consumption and warns when approaching the limit.
621 * Helps prevent out-of-memory errors and performance issues.
622 *
623 * Status levels:
624 * - good: <=90% memory usage
625 * - critical: >90% memory usage
626 *
627 * @since 1.0.0
628 * @return array Site Health test result
629 */
630 public function memory_usage_check()
631 {
632 $stats = $this->get_memory_stats();
633
634 // Handle unlimited memory
635 if ($stats['limit_bytes'] === -1) {
636 return [
637 'label' => __('PHP memory limit is unlimited'),
638 'status' => 'good',
639 'badge' => [
640 'label' => Metasync::get_effective_plugin_name(),
641 'color' => 'green'
642 ],
643 'description' => sprintf(
644 '<p>%s</p>' .
645 '<p><strong>%s</strong></p>' .
646 '<ul>' .
647 '<li>%s %s</li>' .
648 '<li>%s %s</li>' .
649 '</ul>',
650 __('PHP memory limit is set to unlimited. While this prevents memory errors, it may allow poorly optimized code to consume excessive server resources.'),
651 __('Current Memory Usage:'),
652 __('Current usage:'),
653 $stats['current_formatted'],
654 __('Peak usage:'),
655 $stats['peak_formatted']
656 ),
657 'test' => 'metasync_memory_usage'
658 ];
659 }
660
661 $percentage = $stats['percentage'];
662
663 // Determine status based on memory usage percentage
664 if ($percentage > self::MEMORY_CRITICAL_THRESHOLD) {
665 $status = 'critical';
666 $label = __('PHP memory usage is critically high');
667 } else {
668 $status = 'good';
669 $label = __('PHP memory usage is healthy');
670 }
671
672 // Build detailed description
673 $description = sprintf(
674 '<p><strong>%s</strong></p>' .
675 '<ul>' .
676 '<li>%s %s (%s)</li>' .
677 '<li>%s %s</li>' .
678 '<li>%s %s</li>' .
679 '<li>%s <strong>%.2f%%</strong></li>' .
680 '<li>%s %s</li>' .
681 '</ul>',
682 __('PHP Memory Statistics:'),
683 __('Memory limit:'),
684 $stats['limit_formatted'],
685 $stats['limit'],
686 __('Current usage:'),
687 $stats['current_formatted'],
688 __('Peak usage:'),
689 $stats['peak_formatted'],
690 __('Usage percentage:'),
691 $percentage,
692 __('Available memory:'),
693 $stats['available_formatted']
694 );
695
696 // Add context about memory usage
697 if ($status === 'good') {
698 $description .= sprintf(
699 '<p><em>%s</em></p>',
700 __('Memory usage is within acceptable limits. The site has sufficient memory available for normal operations.')
701 );
702 } else {
703 // Critical status - provide actionable recommendations
704 $description .= sprintf(
705 '<p><strong>%s</strong></p>' .
706 '<p>%s</p>' .
707 '<ul>' .
708 '<li>%s</li>' .
709 '<li>%s</li>' .
710 '<li>%s</li>' .
711 '<li>%s</li>' .
712 '</ul>',
713 __('Critical Memory Usage Detected:'),
714 __('PHP memory usage is critically high (>90%). This may cause:'),
715 __('Fatal errors: "Allowed memory size exhausted"'),
716 __('Failed page loads and white screens'),
717 __('Incomplete plugin/theme operations'),
718 __('Performance degradation and slow response times')
719 );
720
721 $description .= sprintf(
722 '<p><strong>%s</strong></p>' .
723 '<ul>' .
724 '<li>%s</li>' .
725 '<li>%s</li>' .
726 '<li>%s</li>' .
727 '<li>%s</li>' .
728 '<li>%s</li>' .
729 '</ul>',
730 __('Recommended Actions:'),
731 sprintf(
732 __('Increase PHP memory limit in wp-config.php: <code>define(\'WP_MEMORY_LIMIT\', \'256M\');</code> (current: %s)'),
733 $stats['limit']
734 ),
735 __('Contact your hosting provider to increase server memory limits'),
736 __('Deactivate unnecessary plugins that consume excessive memory'),
737 __('Optimize images and reduce media library size'),
738 __('Consider upgrading to a hosting plan with more resources')
739 );
740
741 // Add warning if peak usage is also high
742 if ($stats['peak'] > ($stats['limit_bytes'] * 0.95)) {
743 $description .= sprintf(
744 '<p><strong>%s</strong> %s</p>',
745 __('Warning:'),
746 sprintf(
747 __('Peak memory usage (%s) is very close to the limit. Memory errors may occur during high-traffic periods or resource-intensive operations.'),
748 $stats['peak_formatted']
749 )
750 );
751 }
752
753 $description .= sprintf(
754 '<p>%s %s</p>',
755 __( 'Quick link:' ),
756 sprintf(
757 '<a href="%s">%s</a>',
758 esc_url( admin_url( 'plugins.php' ) ),
759 esc_html( __( 'Manage Plugins (deactivate memory-heavy plugins)' ) )
760 )
761 );
762 }
763
764 return [
765 'label' => $label,
766 'status' => $status,
767 'badge' => [
768 'label' => Metasync::get_effective_plugin_name(),
769 'color' => $status === 'critical' ? 'red' : 'green'
770 ],
771 'description' => $description,
772 'test' => 'metasync_memory_usage'
773 ];
774 }
775
776 /**
777 * Get OTTO API rate limit statistics for the last 24 hours
778 *
779 * Reads the rolling timestamp log written by metasync_record_otto_rate_limit_hit()
780 * and counts how many 429 responses were received in the past 24 hours.
781 *
782 * @since 1.0.0
783 * @return array Array with 'hits_24h' (int) and 'last_hit' (int|null)
784 */
785 private function get_rate_limit_stats()
786 {
787 $log = get_option('metasync_otto_rate_limit_log', []);
788 $cutoff = time() - DAY_IN_SECONDS;
789
790 $recent = array_filter($log, function($t) use ($cutoff) { return $t > $cutoff; });
791
792 return [
793 'hits_24h' => count($recent),
794 'last_hit' => !empty($recent) ? max($recent) : null,
795 ];
796 }
797
798 /**
799 * Main Site Health test for SearchAtlas OTTO API rate limits
800 *
801 * Checks how frequently the OTTO_URL_DETAILS endpoint has returned HTTP 429
802 * (Too Many Requests) in the last 24 hours and surfaces a warning when the
803 * rate limit is being hit too often.
804 *
805 * Status levels:
806 * - good: < 5 hits in the last 24 hours
807 * - recommended: 5–19 hits in the last 24 hours
808 * - critical: >= 20 hits in the last 24 hours
809 *
810 * @since 1.0.0
811 * @return array Site Health test result
812 */
813 public function otto_rate_limit_check()
814 {
815 $stats = $this->get_rate_limit_stats();
816 $hits = $stats['hits_24h'];
817 $last_hit = $stats['last_hit'];
818
819 // Format the last hit timestamp if available
820 $last_hit_text = $last_hit
821 ? sprintf(
822 __('Last occurrence: %s'),
823 date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_hit)
824 )
825 : __('No rate limit hits recorded.');
826
827 if ($hits >= self::RATE_LIMIT_CRITICAL_THRESHOLD) {
828 $status = 'critical';
829 $label = sprintf(
830 __('%s OTTO API is being rate limited frequently (%d times in 24h)'),
831 Metasync::get_effective_plugin_name(),
832 $hits
833 );
834 } elseif ($hits >= self::RATE_LIMIT_WARNING_THRESHOLD) {
835 $status = 'recommended';
836 $label = sprintf(
837 __('%s OTTO API rate limit hit %d times in the last 24 hours'),
838 Metasync::get_effective_plugin_name(),
839 $hits
840 );
841 } else {
842 $status = 'good';
843 $label = sprintf(__('%s OTTO API rate limits are within normal range'), Metasync::get_effective_plugin_name());
844 }
845
846 // Build description
847 $description = sprintf(
848 '<p><strong>%s</strong></p>' .
849 '<ul>' .
850 '<li>%s <strong>%d</strong></li>' .
851 '<li>%s</li>' .
852 '</ul>',
853 sprintf(__('%s API Rate Limit Statistics (last 24 hours):'), Metasync::get_whitelabel_otto_name()),
854 __('HTTP 429 responses received:'),
855 $hits,
856 $last_hit_text
857 );
858
859 if ($status === 'good') {
860 $description .= sprintf(
861 '<p><em>%s</em></p>',
862 sprintf(__('The %s API is responding normally. No action required.'), Metasync::get_whitelabel_otto_name())
863 );
864 } else {
865 $description .= sprintf(
866 '<p><strong>%s</strong></p>' .
867 '<p>%s</p>' .
868 '<ul>' .
869 '<li>%s</li>' .
870 '<li>%s</li>' .
871 '<li>%s</li>' .
872 '</ul>',
873 sprintf(__('Frequent rate limiting can degrade %s SEO data delivery on your site.'), Metasync::get_whitelabel_otto_name()),
874 __('Recommended Actions:'),
875 sprintf(__('Review your %1$s crawl frequency settings in the %2$s dashboard to reduce request volume.'), Metasync::get_whitelabel_otto_name(), Metasync::get_effective_plugin_name()),
876 sprintf(__('Check if multiple server processes are triggering %s jobs simultaneously.'), Metasync::get_whitelabel_otto_name()),
877 sprintf(__('Contact %s support if rate limiting persists despite low crawl volume.'), Metasync::get_effective_plugin_name())
878 );
879
880 $description .= sprintf(
881 '<p>%s %s</p>',
882 __( 'Quick link:' ),
883 $this->get_admin_link( '&tab=general', __( 'Review API Settings' ) )
884 );
885 }
886
887 return [
888 'label' => $label,
889 'status' => $status,
890 'badge' => [
891 'label' => Metasync::get_effective_plugin_name(),
892 'color' => $status === 'critical' ? 'red' : ( $status === 'recommended' ? 'orange' : 'green' ),
893 ],
894 'description' => $description,
895 'test' => 'metasync_otto_rate_limit',
896 ];
897 }
898
899 /**
900 * Get current debug mode status across all relevant constants.
901 *
902 * Reads live PHP constants so the result reflects what PHP is
903 * actually executing, even when wp-config.php was edited manually.
904 *
905 * @since 1.0.0
906 * @return array {
907 * @type bool $wp_debug True if WP_DEBUG is defined and truthy.
908 * @type bool $wp_debug_log True if WP_DEBUG_LOG is defined and truthy.
909 * @type bool $wp_debug_display True if WP_DEBUG_DISPLAY is defined and truthy.
910 * @type bool $metasync_debug True if METASYNC_DEBUG is defined and truthy.
911 * }
912 */
913 private function get_debug_stats()
914 {
915 return [
916 'wp_debug' => defined('WP_DEBUG') && WP_DEBUG,
917 'wp_debug_log' => defined('WP_DEBUG_LOG') && WP_DEBUG_LOG,
918 'wp_debug_display' => defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY,
919 'metasync_debug' => defined('METASYNC_DEBUG') && constant('METASYNC_DEBUG'),
920 ];
921 }
922
923 /**
924 * Main Site Health test for WordPress debug mode.
925 *
926 * Checks whether WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY, or
927 * METASYNC_DEBUG are active. Debug mode is normal in development
928 * but should be disabled on production sites to avoid exposing
929 * server internals to visitors and incurring performance overhead.
930 *
931 * Status levels:
932 * - good: No debug constants are active.
933 * - recommended: One or more debug constants are active.
934 *
935 * The 'recommended' (not 'critical') status is intentional: debug mode
936 * is a configuration choice, not a breakage, and should not alarm users
937 * who are legitimately running a staging environment.
938 *
939 * @since 1.0.0
940 * @return array Site Health test result
941 */
942 public function debug_mode_check()
943 {
944 $stats = $this->get_debug_stats();
945 $any_debug_on = $stats['wp_debug'] || $stats['metasync_debug'];
946
947 // Good state: no debug constants active
948 if ( ! $any_debug_on ) {
949 return [
950 'label' => __('Debug mode is disabled'),
951 'status' => 'good',
952 'badge' => [
953 'label' => Metasync::get_effective_plugin_name(),
954 'color' => 'green',
955 ],
956 'description' => sprintf(
957 '<p>%s</p>',
958 __('WordPress debug mode is not active. This is the recommended configuration for production sites.')
959 ),
960 'test' => 'metasync_debug_mode',
961 ];
962 }
963
964 // Recommended state: one or more debug constants are active
965 if ( $stats['wp_debug_display'] ) {
966 $label = __('Debug mode is enabled with error display active');
967 } else {
968 $label = __('Debug mode is enabled on this site');
969 }
970
971 // Status table showing each flag's current state
972 $description = sprintf(
973 '<p><strong>%s</strong></p>' .
974 '<ul>' .
975 '<li>%s <strong>%s</strong></li>' .
976 '<li>%s <strong>%s</strong></li>' .
977 '<li>%s <strong>%s</strong></li>' .
978 '</ul>',
979 __('Active Debug Configuration:'),
980 __('WP_DEBUG:'),
981 $stats['wp_debug'] ? __('Enabled') : __('Disabled'),
982 __('WP_DEBUG_LOG:'),
983 $stats['wp_debug_log'] ? __('Enabled') : __('Disabled'),
984 __('WP_DEBUG_DISPLAY:'),
985 $stats['wp_debug_display'] ? __('Enabled') : __('Disabled')
986 );
987
988 // Call out METASYNC_DEBUG if it is separately active
989 if ( $stats['metasync_debug'] ) {
990 $description .= sprintf(
991 '<p>%s</p>',
992 __('The <code>METASYNC_DEBUG</code> constant is also active. This enables additional MetaSync diagnostic output.')
993 );
994 }
995
996 // Escalated warning when WP_DEBUG_DISPLAY is on (errors visible to site visitors)
997 if ( $stats['wp_debug_display'] ) {
998 $description .= sprintf(
999 '<p><strong>%s</strong> %s</p>',
1000 __('Warning:'),
1001 __('WP_DEBUG_DISPLAY is active. PHP errors and notices may be visible to your site visitors, potentially exposing file paths, database details, and plugin internals. This should be disabled on any publicly accessible site.')
1002 );
1003 }
1004
1005 $description .= sprintf(
1006 '<p><em>%s</em></p>',
1007 __('Debug mode is intended for development environments. On a production site, disabling it prevents information leakage and removes the performance overhead of error collection.')
1008 );
1009
1010 $description .= sprintf(
1011 '<p><strong>%s</strong></p>' .
1012 '<ul>' .
1013 '<li>%s</li>' .
1014 '<li>%s</li>' .
1015 '</ul>',
1016 __('Recommended Actions:'),
1017 __('Set <code>define(\'WP_DEBUG\', false);</code> in your wp-config.php if this is a live production site.'),
1018 __('If error logging is needed in production, avoid WP_DEBUG_LOG — it writes to <code>wp-content/debug.log</code> which is publicly accessible via browser and can expose file paths, database details, and API keys. Use a dedicated logging solution that writes outside the webroot instead.')
1019 );
1020
1021 $description .= sprintf(
1022 '<p>%s %s</p>',
1023 __( 'Quick link:' ),
1024 $this->get_admin_link( '&tab=advanced', __( 'Advanced Settings' ) )
1025 );
1026
1027 return [
1028 'label' => $label,
1029 'status' => 'recommended',
1030 'badge' => [
1031 'label' => Metasync::get_effective_plugin_name(),
1032 'color' => 'orange',
1033 ],
1034 'description' => $description,
1035 'test' => 'metasync_debug_mode',
1036 ];
1037 }
1038
1039 /**
1040 * Get current performance settings for display in the Site Health Info tab
1041 *
1042 * Reads PHP configuration, WordPress constants, class constants, and scheduled
1043 * event timing to give administrators a single-glance view of the settings that
1044 * most directly affect MetaSync's runtime performance.
1045 *
1046 * @since 1.0.0
1047 * @return array Associative array of performance setting fields ready for the info tab
1048 */
1049 private function get_performance_settings()
1050 {
1051 // WP-Cron mode
1052 $wp_cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
1053 $wp_cron_mode = $wp_cron_disabled
1054 ? __( 'System Cron (DISABLE_WP_CRON = true)' )
1055 : __( 'WP-Cron (default)' );
1056
1057 // PHP max execution time
1058 $max_exec_raw = ini_get( 'max_execution_time' );
1059 $max_exec = $max_exec_raw === '0' || $max_exec_raw === 0
1060 ? __( 'Unlimited (0)' )
1061 : (int) $max_exec_raw . 's';
1062
1063 // Next scheduled transient cleanup
1064 $next_cleanup_ts = wp_next_scheduled( 'metasync_cleanup_transients' );
1065 $next_cleanup_val = $next_cleanup_ts
1066 ? wp_date( 'Y-m-d H:i:s', $next_cleanup_ts ) . ' (in ' . human_time_diff( $next_cleanup_ts ) . ')'
1067 : __( 'Not scheduled' );
1068
1069 // Telemetry flush interval label
1070 $flush_interval = Metasync_Telemetry_Config::QUEUE_FLUSH_INTERVAL;
1071 $schedules = wp_get_schedules();
1072 $flush_interval_label = isset( $schedules[ $flush_interval ]['display'] )
1073 ? $schedules[ $flush_interval ]['display']
1074 : $flush_interval;
1075
1076 // OTTO suggestions cache TTL (stored in seconds, display as minutes)
1077 $otto_cache_ttl_sec = Metasync_Otto_Config::get_otto_cache_ttl_seconds();
1078 $otto_cache_ttl_val = ( $otto_cache_ttl_sec / MINUTE_IN_SECONDS ) . ' ' . __( 'minutes' );
1079
1080 return [
1081 'perf_wp_cron' => [
1082 'label' => __( 'Performance - WP-Cron Mode' ),
1083 'value' => $wp_cron_mode,
1084 ],
1085 'perf_php_max_exec' => [
1086 'label' => __( 'Performance - PHP Max Execution Time' ),
1087 'value' => $max_exec,
1088 ],
1089 'perf_otto_request_timeout' => [
1090 'label' => sprintf(__('Performance - %s API Request Timeout'), Metasync::get_whitelabel_otto_name()),
1091 'value' => '30s',
1092 ],
1093 'perf_otto_cache_ttl' => [
1094 'label' => sprintf(__('Performance - %s Suggestions Cache TTL'), Metasync::get_whitelabel_otto_name()),
1095 'value' => $otto_cache_ttl_val,
1096 ],
1097 'perf_otto_max_calls' => [
1098 'label' => sprintf(__('Performance - %s Max API Calls/Min'), Metasync::get_whitelabel_otto_name()),
1099 'value' => Metasync_Otto_Transient_Cache::MAX_API_CALLS_PER_MINUTE,
1100 ],
1101 'perf_telemetry_queue_max' => [
1102 'label' => __( 'Performance - Telemetry Max Queue Size' ),
1103 'value' => Metasync_Telemetry_Config::MAX_QUEUE_SIZE,
1104 ],
1105 'perf_telemetry_batch' => [
1106 'label' => __( 'Performance - Telemetry Batch Size' ),
1107 'value' => Metasync_Telemetry_Config::QUEUE_BATCH_SIZE,
1108 ],
1109 'perf_telemetry_flush' => [
1110 'label' => __( 'Performance - Telemetry Queue Flush Interval' ),
1111 'value' => $flush_interval_label,
1112 ],
1113 'perf_api_max_retries' => [
1114 'label' => __( 'Performance - API Max Retry Attempts' ),
1115 'value' => Metasync_Telemetry_Config::MAX_RETRY_ATTEMPTS,
1116 ],
1117 'perf_next_cleanup' => [
1118 'label' => __( 'Performance - Next Transient Cleanup' ),
1119 'value' => $next_cleanup_val,
1120 ],
1121 ];
1122 }
1123
1124 /**
1125 * Get failed MetaSync cron action statistics for the last 24 hours
1126 *
1127 * Reads the rolling timestamp log written by metasync_record_failed_action()
1128 * and counts how many failures were recorded in the past 24 hours.
1129 *
1130 * @since 1.0.0
1131 * @return array Array with 'count' (int) and 'last_hit' (int|null)
1132 */
1133 private function get_failed_actions_stats()
1134 {
1135 $log = get_option( 'metasync_failed_actions_log', [] );
1136 $cutoff = time() - DAY_IN_SECONDS;
1137
1138 $recent = array_filter( $log, function( $t ) use ( $cutoff ) { return $t > $cutoff; } );
1139
1140 return [
1141 'count' => count( $recent ),
1142 'last_hit' => ! empty( $recent ) ? max( $recent ) : null,
1143 ];
1144 }
1145
1146 /**
1147 * Main Site Health test for MetaSync failed cron actions
1148 *
1149 * Checks how many MetaSync OTTO SEO processing jobs have failed in the last
1150 * 24 hours and surfaces a warning when failures exceed the threshold.
1151 * Failures are recorded by metasync_record_failed_action() in otto_pixel.php
1152 * when metasync_process_otto_seo_data() encounters an API error or exception.
1153 *
1154 * Status levels:
1155 * - good: <= 100 failures in the last 24 hours
1156 * - recommended: > 100 failures in the last 24 hours
1157 *
1158 * @since 1.0.0
1159 * @return array Site Health test result
1160 */
1161 public function failed_actions_check()
1162 {
1163 $stats = $this->get_failed_actions_stats();
1164 $count = $stats['count'];
1165 $last_hit = $stats['last_hit'];
1166
1167 $last_hit_text = $last_hit
1168 ? sprintf(
1169 __( 'Last failure: %s' ),
1170 date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_hit )
1171 )
1172 : __( 'No failed actions recorded in the last 24 hours.' );
1173
1174 if ( $count > self::FAILED_ACTIONS_THRESHOLD ) {
1175 $status = 'recommended';
1176 $label = sprintf(
1177 __( '%s has %d failed SEO processing actions in the last 24 hours' ),
1178 Metasync::get_effective_plugin_name(),
1179 $count
1180 );
1181 } else {
1182 $status = 'good';
1183 $label = sprintf(__('%s SEO action queue is healthy'), Metasync::get_effective_plugin_name());
1184 }
1185
1186 $description = sprintf(
1187 '<p><strong>%s</strong></p>' .
1188 '<ul>' .
1189 '<li>%s <strong>%d</strong></li>' .
1190 '<li>%s</li>' .
1191 '</ul>',
1192 sprintf(__('%s Failed Actions Statistics (last 24 hours):'), Metasync::get_effective_plugin_name()),
1193 sprintf(__('Failed %s SEO processing jobs:'), Metasync::get_whitelabel_otto_name()),
1194 $count,
1195 $last_hit_text
1196 );
1197
1198 if ( $status === 'good' ) {
1199 $description .= sprintf(
1200 '<p><em>%s</em></p>',
1201 sprintf(__('%s SEO processing jobs are completing successfully. No action required.'), Metasync::get_whitelabel_otto_name())
1202 );
1203 } else {
1204 $description .= sprintf(
1205 '<p><strong>%s</strong></p>' .
1206 '<ul>' .
1207 '<li>%s</li>' .
1208 '<li>%s</li>' .
1209 '<li>%s</li>' .
1210 '</ul>',
1211 __( 'Recommended Actions:' ),
1212 sprintf(__('Check your %1$s API connection in %1$s → Settings.'), Metasync::get_effective_plugin_name()),
1213 sprintf(__('Review your PHP error log for %s processing exceptions.'), Metasync::get_whitelabel_otto_name()),
1214 sprintf(__('Verify your %1$s UUID is correctly configured and the %2$s API is reachable.'), Metasync::get_whitelabel_otto_name(), Metasync::get_effective_plugin_name())
1215 );
1216
1217 $description .= sprintf(
1218 '<p>%s %s &middot; %s</p>',
1219 __( 'Quick links:' ),
1220 $this->get_admin_link( '&tab=general', __( 'Review API Settings' ) ),
1221 $this->get_admin_link( '-sync-log', __( 'View Changes Log' ) )
1222 );
1223 }
1224
1225 return [
1226 'label' => $label,
1227 'status' => $status,
1228 'badge' => [
1229 'label' => Metasync::get_effective_plugin_name(),
1230 'color' => $status === 'recommended' ? 'orange' : 'green',
1231 ],
1232 'description' => $description,
1233 'test' => 'metasync_failed_actions',
1234 ];
1235 }
1236
1237 /**
1238 * Site Health test for OTTO crawl-notify background job reliability
1239 *
1240 * Reads the bounded per-URL status store (Metasync_Otto_Job_Status) that
1241 * the crawl-notify pipeline writes as its background jobs run, so an
1242 * administrator can see what actually happened to notified URLs — not just
1243 * that the webhook was acknowledged.
1244 *
1245 * Status levels:
1246 * - good: no failed URLs and fewer than the threshold stuck retrying
1247 * - recommended: failed URLs exist or retrying count exceeds the threshold
1248 *
1249 * When WP-Cron is disabled (DISABLE_WP_CRON) without a visible system
1250 * crontab entry the pipeline cannot run at all, so an advisory is always
1251 * included in that case.
1252 *
1253 * @since 1.0.0
1254 * @return array Site Health test result
1255 */
1256 public function otto_job_reliability_check()
1257 {
1258 $otto_name = Metasync::get_whitelabel_otto_name();
1259
1260 if ( ! class_exists( 'Metasync_Otto_Job_Status' ) ) {
1261 return [
1262 'label' => sprintf( __( '%s crawl job tracking is unavailable' ), $otto_name ),
1263 'status' => 'good',
1264 'badge' => [
1265 'label' => Metasync::get_effective_plugin_name(),
1266 'color' => 'green',
1267 ],
1268 'description' => '<p>' . esc_html__( 'The status store class is not loaded; nothing to report.' ) . '</p>',
1269 'test' => 'metasync_otto_job_reliability',
1270 ];
1271 }
1272
1273 $counts = Metasync_Otto_Job_Status::counts();
1274
1275 $retrying_threshold = 25;
1276 if ( $counts['failed'] > 0 || $counts['retrying'] > $retrying_threshold ) {
1277 $status = 'recommended';
1278 $label = sprintf(
1279 __( '%s crawl jobs need attention (%d failed, %d retrying)' ),
1280 $otto_name,
1281 $counts['failed'],
1282 $counts['retrying']
1283 );
1284 } else {
1285 $status = 'good';
1286 $label = sprintf( __( '%s crawl jobs are completing normally' ), $otto_name );
1287 }
1288
1289 $description = sprintf(
1290 '<p><strong>%s</strong></p><ul><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li></ul>',
1291 sprintf( __( '%s crawl job status (last 48 hours):' ), $otto_name ),
1292 __( 'Accepted, awaiting run:' ),
1293 $counts['accepted'],
1294 __( 'Retrying after transient failures:' ),
1295 $counts['retrying'],
1296 __( 'Completed:' ),
1297 $counts['completed'],
1298 __( 'Failed (retries exhausted):' ),
1299 $counts['failed'],
1300 __( 'Queued past the per-batch cap:' ),
1301 $counts['pending']
1302 );
1303
1304 if ( $this->constant_is_truthy( 'DISABLE_WP_CRON' ) ) {
1305 $description .= sprintf(
1306 '<p><strong>%s</strong> %s</p>',
1307 __( 'WP-Cron is disabled on this site.' ),
1308 sprintf(
1309 /* translators: %s: OTTO product name */
1310 __( 'Background jobs only run if a system crontab (or equivalent) calls wp-cron.php. Without it, %s crawl notifications are queued but never processed.' ),
1311 $otto_name
1312 )
1313 );
1314 }
1315
1316 if ( $status !== 'good' ) {
1317 $description .= sprintf(
1318 '<p>%s %s &middot; %s</p>',
1319 __( 'Quick links:' ),
1320 $this->get_admin_link( '&tab=general', __( 'Review API Settings' ) ),
1321 $this->get_admin_link( '-sync-log', __( 'View Changes Log' ) )
1322 );
1323 }
1324
1325 return [
1326 'label' => $label,
1327 'status' => $status,
1328 'badge' => [
1329 'label' => Metasync::get_effective_plugin_name(),
1330 'color' => $status === 'recommended' ? 'orange' : 'green',
1331 ],
1332 'description' => $description,
1333 'test' => 'metasync_otto_job_reliability',
1334 ];
1335 }
1336
1337 /**
1338 * Whether a named constant is defined and evaluates truthy.
1339 *
1340 * The constant name arrives as data so the value is resolved through
1341 * constant() at runtime; some constants are also loaded from WP stubs
1342 * with fixed values, which makes a direct `defined() && CONST` check
1343 * statically redundant even though the live value varies per site.
1344 *
1345 * @since 1.0.0
1346 * @param string $name Constant name, without the leading backslash.
1347 * @return bool
1348 */
1349 private function constant_is_truthy( $name ) {
1350 return defined( $name ) && filter_var( constant( $name ), FILTER_VALIDATE_BOOLEAN );
1351 }
1352 }
1353