Files
2meet-data-optimizer/includes/diagnostic/class-tmdo-monthly-summary.php
T
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

284 lines
9.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* TMDO_Monthly_Summary — 30-day rollup report (v2.4.0 M11).
*
* Generates a monthly executive summary covering
* - Health success / fail / rate-limit ratios over last 30 days
* - Zone size growth (deltas from 30 days ago snapshot if available)
* - Per-module FSM trajectory (who promoted, who rolled back)
* - Snapshot retention overview (count, oldest, newest)
*
* Hooks the existing `wpdo_health_snapshot_monthly` action (Core line ~489)
* so it runs once per month at 03:00 UTC on the 1st. Output
* 1. Persisted to wp_wpdo_audit (op='monthly_summary').
* 2. Stored as wp_options.wpdo_monthly_summary_latest (autoload=no).
* 3. Rendered on the admin Doctor tab "📅 Monthly Summary" sub-section.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Monthly summary aggregator.
*/
class TMDO_Monthly_Summary {
public const OPTION_LATEST = 'wpdo_monthly_summary_latest';
/**
* Hook into Core's existing monthly cron.
*
* @return void
*/
public static function register(): void {
add_action( 'wpdo_health_snapshot_monthly', array( __CLASS__, 'generate' ), 20 );
}
/**
* Build + persist the monthly summary.
*
* @return array Summary structure.
*/
public static function generate(): array {
$started_at = microtime( true );
$summary = array(
'period_start' => gmdate( 'Y-m-d H:i:s', strtotime( '-30 days' ) ),
'period_end' => gmdate( 'Y-m-d H:i:s' ),
'health' => self::aggregate_health(),
'fsm_trajectory' => self::aggregate_fsm(),
'snapshots' => self::aggregate_snapshots(),
'zone_growth' => self::aggregate_zone_growth(),
'autoload_size' => self::measure_autoload(),
'duration_ms' => 0,
'generated_at' => gmdate( 'Y-m-d H:i:s' ),
);
$summary['duration_ms'] = (int) round( ( microtime( true ) - $started_at ) * 1000 );
update_option( self::OPTION_LATEST, $summary, false );
// v2.5.0 M14: archive into history (max 12 entries).
$history = (array) get_option( 'wpdo_monthly_summary_history', array() );
array_unshift( $history, $summary );
$history = array_slice( $history, 0, 12 );
update_option( 'wpdo_monthly_summary_history', $history, false );
if ( class_exists( 'TMDO_Logger' ) ) {
TMDO_Logger::info(
'monthly_summary',
array(
'health_success' => (int) ( $summary['health']['success'] ?? 0 ),
'health_critical' => (int) ( $summary['health']['critical'] ?? 0 ),
'fsm_promotions' => count( $summary['fsm_trajectory']['promotions'] ?? array() ),
'fsm_rollbacks' => count( $summary['fsm_trajectory']['rollbacks'] ?? array() ),
'snapshots_total' => (int) ( $summary['snapshots']['total'] ?? 0 ),
'autoload_kb' => (int) round( ( $summary['autoload_size'] ?? 0 ) / 1024 ),
)
);
}
do_action( 'wpdo/monthly_summary_generated', $summary );
return $summary;
}
/**
* Read the latest summary (from wp_options).
*
* @return array|null
*/
public static function get_latest(): ?array {
$v = get_option( self::OPTION_LATEST );
return is_array( $v ) ? $v : null;
}
// ─── private ────────────────────────────────────────────────────
/**
* Aggregate health-check audit log entries from last 30 days.
*
* @return array {success:int, recommended:int, critical:int, total:int}
*/
private static function aggregate_health(): array {
global $wpdb;
$out = array(
'success' => 0,
'recommended' => 0,
'critical' => 0,
'total' => 0,
);
// wp_wpdo_audit may not exist on early v1.x installs; check first.
$audit = $wpdb->prefix . 'wpdo_audit';
$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',
$audit
)
);
if ( 0 === $exists ) {
return $out;
}
// Health check audit rows have op='health_check_daily'.
$rows = (array) $wpdb->get_results( "SELECT * FROM `{$audit}` WHERE op = 'health_check_daily' AND ts >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY)", ARRAY_A ); // phpcs:ignore WordPress.DB
foreach ( $rows as $r ) {
$ctx = isset( $r['value_after'] ) ? json_decode( (string) $r['value_after'], true ) : null;
if ( ! is_array( $ctx ) ) {
continue;
}
$crit = (int) ( $ctx['critical'] ?? 0 );
$rec = (int) ( $ctx['recommended'] ?? 0 );
++$out['total'];
if ( $crit > 0 ) {
++$out['critical'];
} elseif ( $rec > 0 ) {
++$out['recommended'];
} else {
++$out['success'];
}
}
return $out;
}
/**
* Best-effort FSM trajectory: which modules changed state in the last 30
* days. Reads `wpdo_fsm_state_entered` + current state.
*
* @return array {promotions:array, rollbacks:array, stationary:array}
*/
private static function aggregate_fsm(): array {
$out = array(
'promotions' => array(),
'rollbacks' => array(),
'stationary' => array(),
);
if ( ! class_exists( 'TMDO_Feature_Flags' ) ) {
return $out;
}
$entered = (array) get_option( 'wpdo_fsm_state_entered', array() );
$now = time();
foreach ( TMDO_Feature_Flags::all() as $module => $state ) {
$ts = isset( $entered[ $module ]['entered_at'] ) ? strtotime( (string) $entered[ $module ]['entered_at'] . ' UTC' ) : 0;
$age_days = $ts > 0 ? (int) floor( ( $now - $ts ) / DAY_IN_SECONDS ) : null;
if ( $ts > 0 && ( $now - $ts ) <= ( 30 * DAY_IN_SECONDS ) ) {
// Recently changed — bucket by direction.
if ( 'idle' === $state ) {
$out['rollbacks'][ $module ] = array(
'state' => $state,
'days_in_state' => $age_days,
);
} else {
$out['promotions'][ $module ] = array(
'state' => $state,
'days_in_state' => $age_days,
);
}
} else {
$out['stationary'][ $module ] = array(
'state' => $state,
'days_in_state' => $age_days,
);
}
}
return $out;
}
/**
* Aggregate snapshot retention stats from the snapshots table.
*
* @return array {total:int, oldest:string|null, newest:string|null, total_bytes:int}
*/
private static function aggregate_snapshots(): array {
global $wpdb;
$table = $wpdb->prefix . 'wpdo_snapshots';
$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 array(
'total' => 0,
'oldest' => null,
'newest' => null,
'total_bytes' => 0,
);
}
$row = $wpdb->get_row( "SELECT COUNT(*) AS c, MIN(created_at) AS oldest, MAX(created_at) AS newest, COALESCE(SUM(size_bytes),0) AS bytes FROM `{$table}`", ARRAY_A ); // phpcs:ignore WordPress.DB
return array(
'total' => (int) ( $row['c'] ?? 0 ),
'oldest' => isset( $row['oldest'] ) ? (string) $row['oldest'] : null,
'newest' => isset( $row['newest'] ) ? (string) $row['newest'] : null,
'total_bytes' => (int) ( $row['bytes'] ?? 0 ),
);
}
/**
* Per-zone row count snapshot. Compares to previous month's value when
* available (delta_rows positive = growth).
*
* @return array<string,array>
*/
private static function aggregate_zone_growth(): array {
// Use historical site_metrics when available — avoids live COUNT(*) on large tables
// and provides a 30-day delta (current oldest snapshot).
if ( class_exists( 'TMDO_Site_Metrics_Collector' ) ) {
$latest = TMDO_Site_Metrics_Collector::get_latest_snapshot();
if ( ! empty( $latest ) ) {
// Get oldest snapshot within 30 days for delta calculation.
$keys_of_interest = array( 'eav.postmeta_rows', 'flat.hot_rows', 'flat.cold_rows', 'flat.warm_rows', 'custom_tables.total_rows' );
$out = array();
foreach ( $keys_of_interest as $mk ) {
if ( ! isset( $latest[ $mk ] ) ) {
continue;
}
$history = TMDO_Site_Metrics_Collector::get_history( $mk, 30 );
$oldest = ! empty( $history ) ? (int) $history[0]['value'] : null;
$current = (int) $latest[ $mk ];
$out[ $mk ] = array(
'rows' => $current,
'delta_rows' => null !== $oldest ? $current - $oldest : null,
);
}
return $out;
}
}
// Fallback: live COUNT(*) for warm + archive tables (pre-v2.6.2 installs).
global $wpdb;
$out = array();
$keys = array( 'wpdo_warm', 'wpdo_archive' );
foreach ( $keys as $slug ) {
$table = $wpdb->prefix . $slug;
$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 ) {
continue;
}
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB
$out[ $slug ] = array(
'rows' => $count,
'delta_rows' => null,
);
}
return $out;
}
/**
* Return the total byte size of autoloaded options.
*
* @return int Bytes in autoloaded options.
*/
private static function measure_autoload(): 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
}
}