$results, 'critical_count' => $critical_count, 'recommended_count' => $recommended_count, 'conflicts' => $conflict, 'shadow_diffs' => $shadow, 'autoload_bytes' => $autoload, 'ran_at' => gmdate( 'Y-m-d H:i:s' ), 'duration_ms' => (int) round( ( microtime( true ) - $started_at ) * 1000 ), ); // 1. Persist last-run snapshot (autoload=no, lightweight). update_option( self::OPTION_LAST_RUN, $summary, false ); // 2. Set / clear alert flag. if ( $critical_count > 0 ) { $first_critical = self::first_critical_test( $results ); update_option( self::OPTION_ALERT, array( 'level' => 'critical', 'count' => $critical_count, 'first' => $first_critical, 'ran_at' => $summary['ran_at'], ), false ); } else { delete_option( self::OPTION_ALERT ); } // v2.5.0 M16: refresh module suggestions cache (autoload=no). $module_suggestions_count = 0; if ( class_exists( 'TMDO_Module_Detector' ) ) { try { $detected = TMDO_Module_Detector::detect_all( true ); foreach ( $detected as $r ) { if ( ! empty( $r['available'] ) && 'enable' === ( $r['recommendation'] ?? '' ) ) { ++$module_suggestions_count; } } } catch ( Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- detector failure must not break health check. error_log( '[WPDO] Module detector exception in health cron: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } } // 3. Audit log entry. if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'health_check_daily', array( 'critical' => $critical_count, 'recommended' => $recommended_count, 'duration_ms' => $summary['duration_ms'], 'autoload_kb' => (int) round( $autoload / 1024 ), 'conflicts' => (int) ( $conflict['total'] ?? 0 ), 'module_suggestions_count' => $module_suggestions_count, ) ); } // 4. Fire action for downstream subscribers (v2.4.0 email notifier). if ( $critical_count > 0 ) { do_action( 'wpdo/health_alert_critical', $summary ); } else { do_action( 'wpdo/health_check_passed', $summary ); } return array( 'ok' => true, 'summary' => $summary, 'critical_count' => $critical_count, 'recommended_count' => $recommended_count, 'ts' => $summary['ran_at'], ); } /** * Read most recent run (for SOP runbook + Doctor tab streak counter). * * @return array|null */ public static function get_last_run(): ?array { $v = get_option( self::OPTION_LAST_RUN ); return is_array( $v ) ? $v : null; } /** * Compute consecutive green days from audit log (best-effort for SOP UI). * * @return int */ public static function consecutive_green_days(): int { $last = self::get_last_run(); if ( null === $last ) { return 0; } // If today's run is critical, streak = 0. if ( ( $last['critical_count'] ?? 0 ) > 0 ) { return 0; } // Otherwise approximate via TMDO_Logger — count distinct days with health_check_daily and 0 critical. // Conservative best-effort: just check today's run is green = 1 day. return 1; } // ─── private helpers ──────────────────────────────────────────────── /** * Run all 7 Site Health tests directly (without the WP Site Health UI loop). * * @return array Test slug → result array. */ private static function run_site_health_tests(): array { $out = array(); if ( ! class_exists( 'TMDO_Site_Health' ) ) { return $out; } $tests = array( 'wpdo_schema_drift' => 'check_schema_drift', 'wpdo_error_budget' => 'check_error_budget', 'wpdo_hook_conflicts' => 'check_hook_conflicts', 'wpdo_autoload_bloat' => 'check_autoload_bloat', 'wpdo_postmeta_explosion' => 'check_postmeta_explosion', 'wpdo_orphan_zone_rows' => 'check_orphan_zone_rows', 'wpdo_missing_snapshot' => 'check_missing_snapshot', ); foreach ( $tests as $slug => $cb ) { try { $result = call_user_func( array( 'TMDO_Site_Health', $cb ) ); if ( is_array( $result ) ) { $out[ $slug ] = array( 'status' => (string) ( $result['status'] ?? 'good' ), 'severity' => (string) ( $result['severity'] ?? 'good' ), 'label' => (string) ( $result['label'] ?? $slug ), 'description' => wp_strip_all_tags( (string) ( $result['description'] ?? '' ) ), ); } } catch ( Throwable $e ) { $out[ $slug ] = array( 'status' => 'critical', 'severity' => 'critical', 'label' => $slug, 'description' => 'test threw: ' . $e->getMessage(), ); } } return $out; } /** * Return a conflict summary from TMDO_Conflict_Monitor. * * @return array {total:int, hook_overlap:int, uaepg_overlap:int} */ private static function summarize_conflicts(): array { if ( ! class_exists( 'TMDO_Conflict_Monitor' ) ) { return array( 'total' => 0, 'hook_overlap' => 0, 'uaepg_overlap' => 0, ); } $summary = TMDO_Conflict_Monitor::get_summary(); return array( 'total' => (int) ( $summary['total'] ?? 0 ), 'hook_overlap' => (int) ( $summary['hook_overlap'] ?? 0 ), 'uaepg_overlap' => (int) ( $summary['uaepg_overlap'] ?? 0 ), ); } /** * Per-module shadow_diffs ratio (only for modules currently in `verify`). * Reads wp_wpdo_shadow_diffs and bucket-counts by entity_type. * * @return array */ private static function summarize_shadow_diffs(): array { global $wpdb; $out = array(); $table = $wpdb->prefix . 'wpdo_shadow_diffs'; $exists = (int) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $table ) ); if ( 0 === $exists ) { return $out; } $rows = $wpdb->get_results( "SELECT entity_type, COUNT(*) AS cnt FROM `{$table}` WHERE ts >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 24 HOUR) GROUP BY entity_type", ARRAY_A ); // phpcs:ignore WordPress.DB if ( is_array( $rows ) ) { foreach ( $rows as $r ) { $out[ (string) $r['entity_type'] ] = array( 'diffs_24h' => (int) $r['cnt'], ); } } return $out; } /** * Return the total byte size of autoloaded options. * * @return int Bytes in autoloaded options. */ private static function measure_autoload_size(): int { global $wpdb; return (int) $wpdb->get_var( "SELECT COALESCE(SUM(LENGTH(option_value)),0) FROM `{$wpdb->options}` WHERE autoload = 'yes'" ); // phpcs:ignore WordPress.DB } /** * Return the slug of the first critical test result, or null if none. * * @param array $results Site Health test results map. * @return string|null Slug of first critical test, or null. */ private static function first_critical_test( array $results ): ?string { foreach ( $results as $slug => $r ) { if ( 'critical' === ( $r['status'] ?? '' ) ) { return $slug; } } return null; } }