wpdb = $wpdb; $this->error = JVB()->error(); $this->queue = JVB()->queue(); add_action('jvb_generate_daily_report', [$this, 'generateDailyReport']); } /** * Generate and send a daily report * @return void */ public function generateDailyReport():void { // Collect all metrics $error_stats = $this->gatherErrorStats(); $queue_stats = $this->gatherQueueStats(); $user_metrics = $this->gatherUserMetrics(); $content_metrics = $this->gatherContentMetrics(); $system_health = $this->gatherSystemHealthMetrics(); $notification_metrics = $this->gatherNotificationMetrics(); $user_retention = $this->gatherUserRetentionMetrics(); $favourite_metrics = $this->gatherFavouriteMetrics(); // Format the email content $subject = "[edmonton.ink] Daily System Report - " . date('Y-m-d'); $content = $this->formatDailyReport( $error_stats, $queue_stats, $user_metrics, $content_metrics, $system_health, $notification_metrics, $user_retention, $favourite_metrics ); // Send email $this->sendAdminEmail($subject, $content); // Log successful report generation $this->error->log( 'system_report', 'Daily report generated successfully', [ 'date' => date('Y-m-d'), 'metrics_collected' => [ 'error_stats', 'queue_stats', 'user_metrics', 'content_metrics', 'system_health', 'notification_metrics', 'user_retention', 'favourite_metrics' ] ], 'info' ); } /** * Gather error-related statistics for the past 24 hours * @return array */ protected function gatherErrorStats():array { $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours')); $stats = [ 'total_errors' => 0, 'by_severity' => [], 'by_component' => [], 'peak_hours' => [], 'frequent' => [], 'critical' => [] ]; // Get total count $stats['total_errors'] = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->wpdb->prefix}jvb_error_log WHERE created_at > %s", $yesterday )); // Get counts by severity $severity_counts = $this->wpdb->get_results($this->wpdb->prepare( "SELECT severity, COUNT(*) as count FROM {$this->wpdb->prefix}jvb_error_log WHERE created_at > %s GROUP BY severity ORDER BY count DESC", $yesterday )); foreach ($severity_counts as $row) { $stats['by_severity'][$row->severity] = $row->count; } // Get counts by component $component_counts = $this->wpdb->get_results($this->wpdb->prepare( "SELECT component, COUNT(*) as count FROM {$this->wpdb->prefix}jvb_error_log WHERE created_at > %s GROUP BY component ORDER BY count DESC LIMIT 10", $yesterday )); foreach ($component_counts as $row) { $stats['by_component'][$row->component] = $row->count; } // Get hourly distribution $hour_counts = $this->wpdb->get_results($this->wpdb->prepare( "SELECT HOUR(created_at) as hour, COUNT(*) as count FROM {$this->wpdb->prefix}jvb_error_log WHERE created_at > %s GROUP BY HOUR(created_at) ORDER BY count DESC LIMIT 5", $yesterday )); foreach ($hour_counts as $row) { $stats['peak_hours'][$row->hour] = $row->count; } // Get most frequent errors $stats['frequent'] = $this->wpdb->get_results($this->wpdb->prepare( "SELECT error_type, component, message, COUNT(*) as count FROM {$this->wpdb->prefix}jvb_error_log WHERE created_at > %s GROUP BY error_type, component, message ORDER BY count DESC LIMIT 10", $yesterday )); // Get most recent critical errors $stats['critical'] = $this->wpdb->get_results($this->wpdb->prepare( "SELECT * FROM {$this->wpdb->prefix}jvb_error_log WHERE severity = 'critical' AND created_at > %s ORDER BY created_at DESC LIMIT 5", $yesterday )); return $stats; } /** * Gather statistics from the bulk operation queue * @return array */ protected function gatherQueueStats():array { $table = $this->wpdb->prefix . BASE . 'bulk_operation'; $stats = []; // Get counts by status $status_counts = $this->wpdb->get_results(" SELECT status, COUNT(*) as count, SUM(count) as count FROM $table GROUP BY status "); $stats['by_status'] = []; foreach ($status_counts as $row) { $stats['by_status'][$row->status] = [ 'count' => $row->count, 'operations' => $row->count ]; } // Get counts by operation type $type_counts = $this->wpdb->get_results(" SELECT type, COUNT(*) as count FROM $table GROUP BY type ORDER BY count DESC "); $stats['by_type'] = []; foreach ($type_counts as $row) { $stats['by_type'][$row->type] = $row->count; } // Get counts by priority $priority_counts = $this->wpdb->get_results(" SELECT priority, COUNT(*) as count FROM $table WHERE status = 'pending' GROUP BY priority "); $stats['pending_by_priority'] = []; foreach ($priority_counts as $row) { $stats['pending_by_priority'][$row->priority] = $row->count; } // Check if queue limit has been reached recently $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours')); $limit_reached = $this->wpdb->get_var($this->wpdb->prepare(" SELECT COUNT(*) FROM {$this->wpdb->prefix}jvb_error_log WHERE error_type = 'queue_limit_reached' AND created_at > %s ", $yesterday)); $stats['limit_reached_count'] = $limit_reached; // Get oldest pending operation $oldest_pending = $this->wpdb->get_row(" SELECT id, type, created_at, TIMESTAMPDIFF(HOUR, created_at, NOW()) as age_hours FROM $table WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1 "); if ($oldest_pending) { $stats['oldest_pending'] = [ 'id' => $oldest_pending->id, 'type' => $oldest_pending->type, 'created_at' => $oldest_pending->created_at, 'age_hours' => $oldest_pending->age_hours ]; } // Get failed operations in the last 24 hours $recent_failures = $this->wpdb->get_results($this->wpdb->prepare(" SELECT id, type, error_message FROM $table WHERE status = 'failed' AND completed_at > %s ORDER BY completed_at DESC LIMIT 5 ", $yesterday)); $stats['recent_failures'] = $recent_failures; // Get average processing time by operation type $avg_processing_times = $this->wpdb->get_results(" SELECT type, AVG(TIMESTAMPDIFF(SECOND, started_at, completed_at)) as avg_seconds, COUNT(*) as sample_size FROM $table WHERE status = 'completed' AND started_at IS NOT NULL AND completed_at IS NOT NULL AND completed_at > DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY type HAVING sample_size > 5 "); $stats['avg_processing_times'] = []; foreach ($avg_processing_times as $row) { $stats['avg_processing_times'][$row->type] = [ 'seconds' => round($row->avg_seconds, 2), 'samples' => $row->sample_size ]; } return $stats; } /** * Gather user-related metrics for the past 24 hours * @return array */ protected function gatherUserMetrics():array { $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours')); $metrics = [ 'logins' => [ 'total' => 0, 'by_role' => [] ], 'registrations' => [ 'total' => 0, 'by_role' => [] ], 'approvals' => [ 'total' => 0, 'by_role' => [] ], 'new_shops' => [] ]; // Get login counts from activity log $login_counts = $this->wpdb->get_results($this->wpdb->prepare(" SELECT COUNT(*) as count, MAX(CASE WHEN um.meta_value LIKE '%administrator%' THEN 1 ELSE 0 END) as admin, MAX(CASE WHEN um.meta_value LIKE '%jvb_enthusiast%' THEN 1 ELSE 0 END) as enthusiast, MAX(CASE WHEN um.meta_value LIKE '%jvb_artist%' THEN 1 ELSE 0 END) as artist, MAX(CASE WHEN um.meta_value LIKE '%jvb_partner%' THEN 1 ELSE 0 END) as partner FROM {$this->wpdb->prefix}jvb_activity_log al JOIN {$this->wpdb->usermeta} um ON al.user_id = um.user_id AND um.meta_key = '{$this->wpdb->prefix}capabilities' WHERE al.type = 'login' AND al.created_at > %s GROUP BY al.user_id ", $yesterday)); if (!empty($login_counts)) { foreach ($login_counts as $count) { $metrics['logins']['total'] += $count->count; if ($count->enthusiast) $metrics['logins']['by_role']['enthusiast'] = ($metrics['logins']['by_role']['enthusiast'] ?? 0) + $count->count; if ($count->artist) $metrics['logins']['by_role']['artist'] = ($metrics['logins']['by_role']['artist'] ?? 0) + $count->count; if ($count->partner) $metrics['logins']['by_role']['partner'] = ($metrics['logins']['by_role']['partner'] ?? 0) + $count->count; if ($count->admin) $metrics['logins']['by_role']['admin'] = ($metrics['logins']['by_role']['admin'] ?? 0) + $count->count; } } // Get new user registrations $new_users = $this->wpdb->get_results($this->wpdb->prepare(" SELECT u.ID, u.user_registered, um.meta_value as capabilities FROM {$this->wpdb->users} u JOIN {$this->wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = '{$this->wpdb->prefix}capabilities' WHERE u.user_registered > %s ", $yesterday)); $metrics['registrations']['total'] = count($new_users); foreach ($new_users as $user) { $capabilities = maybe_unserialize($user->capabilities); if (isset($capabilities['jvb_enthusiast'])) { $metrics['registrations']['by_role']['enthusiast'] = ($metrics['registrations']['by_role']['enthusiast'] ?? 0) + 1; } if (isset($capabilities['jvb_artist'])) { $metrics['registrations']['by_role']['artist'] = ($metrics['registrations']['by_role']['artist'] ?? 0) + 1; } if (isset($capabilities['jvb_partner'])) { $metrics['registrations']['by_role']['partner'] = ($metrics['registrations']['by_role']['partner'] ?? 0) + 1; } } // Get user approvals (role changes) $approvals = $this->wpdb->get_results($this->wpdb->prepare(" SELECT al.user_id, al.data, u.user_nicename FROM {$this->wpdb->prefix}jvb_activity_log al JOIN {$this->wpdb->users} u ON al.user_id = u.ID WHERE al.type = 'role_change' AND al.created_at > %s ", $yesterday)); $metrics['approvals']['total'] = count($approvals); foreach ($approvals as $approval) { $data = json_decode($approval->data, true); if (isset($data['new_role'])) { if ($data['new_role'] === 'jvb_artist') { $metrics['approvals']['by_role']['artist'] = ($metrics['approvals']['by_role']['artist'] ?? 0) + 1; $metrics['approvals']['artists'][] = $approval->user_nicename; } elseif ($data['new_role'] === 'jvb_partner') { $metrics['approvals']['by_role']['partner'] = ($metrics['approvals']['by_role']['partner'] ?? 0) + 1; $metrics['approvals']['partners'][] = $approval->user_nicename; } } } // Get new shops $new_shops = $this->wpdb->get_results($this->wpdb->prepare(" SELECT t.term_id, t.name FROM {$this->wpdb->terms} t JOIN {$this->wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE tt.taxonomy = 'jvb_shop' AND tt.description LIKE %s ", '%' . $this->wpdb->esc_like('"created_at":"' . date('Y-m-d', strtotime('-1 day'))) . '%')); foreach ($new_shops as $shop) { $metrics['new_shops'][] = [ 'id' => $shop->term_id, 'name' => $shop->name ]; } $metrics['new_shops_count'] = count($metrics['new_shops']); return $metrics; } /** * Gather content-related metrics for the past 24 hours * @return array */ protected function gatherContentMetrics():array { $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours')); $metrics = [ 'uploads' => [], 'total_uploads' => 0, 'by_user' => [], 'taxonomy_growth' => [] ]; // Define content types to track $content_types = [ 'jvb_tattoo', 'jvb_piercing', 'jvb_artwork', 'jvb_event', 'jvb_offer' ]; // Get counts for each content type foreach ($content_types as $type) { $count = $this->wpdb->get_var($this->wpdb->prepare(" SELECT COUNT(*) FROM {$this->wpdb->posts} WHERE post_type = %s AND post_date > %s ", $type, $yesterday)); $metrics['uploads'][str_replace(BASE, '', $type)] = (int)$count; $metrics['total_uploads'] += (int)$count; } // Get top uploaders $top_uploaders = $this->wpdb->get_results($this->wpdb->prepare(" SELECT post_author, COUNT(*) as count, (SELECT user_nicename FROM {$this->wpdb->users} WHERE ID = post_author) as username, post_type FROM {$this->wpdb->posts} WHERE post_type IN ('" . implode("','", $content_types) . "') AND post_date > %s GROUP BY post_author, post_type ORDER BY count DESC LIMIT 10 ", $yesterday)); foreach ($top_uploaders as $uploader) { if (!isset($metrics['by_user'][$uploader->post_author])) { $metrics['by_user'][$uploader->post_author] = [ 'username' => $uploader->username, 'total' => 0, 'types' => [] ]; } $metrics['by_user'][$uploader->post_author]['types'][str_replace(BASE, '', $uploader->post_type)] = $uploader->count; $metrics['by_user'][$uploader->post_author]['total'] += $uploader->count; } // Sort by total uploads usort($metrics['by_user'], function ($a, $b) { return $b['total'] - $a['total']; }); // Track taxonomy growth $taxonomies = [ 'jvb_style' => 'Tattoo Styles', 'jvb_theme' => 'Tattoo Themes', 'jvb_shop' => 'Shops', 'jvb_city' => 'Cities', 'jvb_artstyle' => 'Art Styles', 'jvb_arttheme' => 'Art Themes', 'jvb_pstyle' => 'Piercing Styles', 'jvb_placement' => 'Placements', ]; foreach ($taxonomies as $taxonomy => $label) { // Get new terms added in the last day $new_terms = $this->wpdb->get_results($this->wpdb->prepare(" SELECT t.term_id, t.name FROM {$this->wpdb->terms} t JOIN {$this->wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE tt.taxonomy = %s AND tt.description LIKE %s ", $taxonomy, '%' . $this->wpdb->esc_like('"created_at":"' . date('Y-m-d', strtotime('-1 day'))) . '%')); if (!empty($new_terms)) { $metrics['taxonomy_growth'][$taxonomy] = [ 'label' => $label, 'count' => count($new_terms), 'terms' => array_map(function ($term) { return [ 'id' => $term->term_id, 'name' => $term->name ]; }, $new_terms) ]; } else { $metrics['taxonomy_growth'][$taxonomy] = [ 'label' => $label, 'count' => 0, 'terms' => [] ]; } } return $metrics; } /** * Gather user retention metrics * @return array */ protected function gatherUserRetentionMetrics():array { $metrics = [ 'active_periods' => [ 'daily' => 0, 'weekly' => 0, 'monthly' => 0 ], 'retention_by_role' => [], 'inactive_counts' => [], 'reactivated_users' => [] ]; // Get daily active users (logged in today) $daily_active = $this->wpdb->get_var(" SELECT COUNT(DISTINCT user_id) FROM {$this->wpdb->prefix}jvb_activity_log WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 DAY) "); $metrics['active_periods']['daily'] = (int)$daily_active; // Get weekly active users $weekly_active = $this->wpdb->get_var(" SELECT COUNT(DISTINCT user_id) FROM {$this->wpdb->prefix}jvb_activity_log WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) "); $metrics['active_periods']['weekly'] = (int)$weekly_active; // Get monthly active users $monthly_active = $this->wpdb->get_var(" SELECT COUNT(DISTINCT user_id) FROM {$this->wpdb->prefix}jvb_activity_log WHERE created_at > DATE_SUB(NOW(), INTERVAL 30 DAY) "); $metrics['active_periods']['monthly'] = (int)$monthly_active; // Get active users by role type in the last week $active_by_role = $this->wpdb->get_results(" SELECT MAX(CASE WHEN um.meta_value LIKE '%administrator%' THEN 1 ELSE 0 END) as admin, MAX(CASE WHEN um.meta_value LIKE '%jvb_enthusiast%' THEN 1 ELSE 0 END) as enthusiast, MAX(CASE WHEN um.meta_value LIKE '%jvb_artist%' THEN 1 ELSE 0 END) as artist, MAX(CASE WHEN um.meta_value LIKE '%jvb_partner%' THEN 1 ELSE 0 END) as partner, COUNT(DISTINCT al.user_id) as count FROM {$this->wpdb->prefix}jvb_activity_log al JOIN {$this->wpdb->usermeta} um ON al.user_id = um.user_id AND um.meta_key = '{$this->wpdb->prefix}capabilities' WHERE al.created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY CASE WHEN um.meta_value LIKE '%administrator%' THEN 'admin' WHEN um.meta_value LIKE '%jvb_artist%' THEN 'artist' WHEN um.meta_value LIKE '%jvb_partner%' THEN 'partner' WHEN um.meta_value LIKE '%jvb_enthusiast%' THEN 'enthusiast' ELSE 'other' END "); foreach ($active_by_role as $role_data) { if ($role_data->admin) $metrics['retention_by_role']['admin'] = $role_data->count; if ($role_data->enthusiast) $metrics['retention_by_role']['enthusiast'] = $role_data->count; if ($role_data->artist) $metrics['retention_by_role']['artist'] = $role_data->count; if ($role_data->partner) $metrics['retention_by_role']['partner'] = $role_data->count; } // Count inactive users by duration $inactive_periods = [ ['30 days', '3 months', 'inactive_1mo_3mo'], ['3 months', '6 months', 'inactive_3mo_6mo'], ['6 months', '1 year', 'inactive_6mo_1yr'], ['1 year', null, 'inactive_1yr_plus'] ]; foreach ($inactive_periods as $period) { $start_period = $period[0]; $end_period = $period[1]; $key = $period[2]; $query = " SELECT COUNT(*) FROM {$this->wpdb->users} u WHERE NOT EXISTS ( SELECT 1 FROM {$this->wpdb->prefix}jvb_activity_log al WHERE al.user_id = u.ID AND al.created_at > DATE_SUB(NOW(), INTERVAL {$start_period}) ) "; if ($end_period) { $query .= " AND EXISTS ( SELECT 1 FROM {$this->wpdb->prefix}jvb_activity_log al WHERE al.user_id = u.ID AND al.created_at > DATE_SUB(NOW(), INTERVAL {$end_period}) )"; } $count = $this->wpdb->get_var($query); $metrics['inactive_counts'][$key] = (int)$count; } // Find reactivated users (inactive for 30+ days who logged in recently) $reactivated = $this->wpdb->get_results(" SELECT al1.user_id, u.user_nicename, MAX(al1.created_at) as recent_login, DATEDIFF( MAX(al1.created_at), ( SELECT MAX(al2.created_at) FROM {$this->wpdb->prefix}jvb_activity_log al2 WHERE al2.user_id = al1.user_id AND al2.created_at < DATE_SUB(MAX(al1.created_at), INTERVAL 30 DAY) ) ) as days_inactive FROM {$this->wpdb->prefix}jvb_activity_log al1 JOIN {$this->wpdb->users} u ON al1.user_id = u.ID WHERE al1.created_at > DATE_SUB(NOW(), INTERVAL 1 DAY) GROUP BY al1.user_id HAVING days_inactive > 30 ORDER BY days_inactive DESC LIMIT 10 "); foreach ($reactivated as $user) { $metrics['reactivated_users'][] = [ 'user_id' => $user->user_id, 'username' => $user->user_nicename, 'days_inactive' => $user->days_inactive, 'recent_login' => $user->recent_login ]; } return $metrics; } /** * Gather favourite patterns metrics * @return array */ protected function gatherFavouriteMetrics():array { $metrics = [ 'total_favourites' => 0, 'favourites_last_24h' => 0, 'by_content_type' => [], 'top_favourited' => [], 'top_artists' => [] ]; $table = $this->wpdb->prefix . BASE . 'favourites'; // Get total favourites $metrics['total_favourites'] = $this->wpdb->get_var(" SELECT COUNT(*) FROM {$table} "); // Get favourites added in the last 24 hours $metrics['favourites_last_24h'] = $this->wpdb->get_var($this->wpdb->prepare(" SELECT COUNT(*) FROM {$table} WHERE date_added > %s ", date('Y-m-d H:i:s', strtotime('-24 hours')))); // Get favourites by content type $by_type = $this->wpdb->get_results(" SELECT type, COUNT(*) as count FROM {$table} GROUP BY type ORDER BY count DESC "); foreach ($by_type as $type) { $metrics['by_content_type'][$type->type] = $type->count; } // Get top favourited content by type $content_types = ['tattoo', 'piercing', 'artwork', 'artist', 'shop']; foreach ($content_types as $type) { $top_items = $this->wpdb->get_results($this->wpdb->prepare(" SELECT f.target_id, COUNT(*) as favourite_count, CASE WHEN %s = 'artist' THEN ( SELECT user_nicename FROM {$this->wpdb->users} WHERE ID = (SELECT post_author FROM {$this->wpdb->posts} WHERE ID = f.target_id) ) WHEN %s = 'shop' THEN ( SELECT name FROM {$this->wpdb->terms} WHERE term_id = f.target_id ) ELSE ( SELECT post_title FROM {$this->wpdb->posts} WHERE ID = f.target_id ) END as name FROM {$table} f WHERE f.type = %s GROUP BY f.target_id ORDER BY favourite_count DESC LIMIT 5 ", $type, $type, $type)); if (!empty($top_items)) { $metrics['top_favourited'][$type] = array_map(function ($item) { return [ 'id' => $item->target_id, 'name' => $item->name, 'count' => $item->favourite_count ]; }, $top_items); } } // Get top artists by favourites received $top_artists = $this->wpdb->get_results(" SELECT p.post_author as user_id, u.user_nicename as artist_name, COUNT(f.id) as favourite_count FROM {$table} f JOIN {$this->wpdb->posts} p ON f.target_id = p.ID JOIN {$this->wpdb->users} u ON p.post_author = u.ID WHERE f.type IN ('tattoo', 'piercing', 'artwork') GROUP BY p.post_author ORDER BY favourite_count DESC LIMIT 10 "); foreach ($top_artists as $artist) { $metrics['top_artists'][] = [ 'user_id' => $artist->user_id, 'name' => $artist->artist_name, 'favourite_count' => $artist->favourite_count ]; } return $metrics; } /** * Gather system health metrics * @return array */ protected function gatherSystemHealthMetrics():array { $metrics = [ 'database' => [ 'size' => 0, 'tables' => [], 'growth' => [] ], 'media' => [ 'total_size' => 0, 'file_count' => 0, 'by_type' => [] ], 'performance' => [ 'average_response_time' => 0, 'slow_queries' => [] ], 'cache' => [ 'hit_rate' => 0, 'size' => 0 ] ]; // Get database size $db_size = $this->wpdb->get_results(" SELECT table_schema as 'database', SUM(data_length + index_length) / 1024 / 1024 as size_mb FROM information_schema.TABLES WHERE table_schema = DATABASE() GROUP BY table_schema "); if (!empty($db_size)) { $metrics['database']['size'] = round($db_size[0]->size_mb, 2); } // Get largest tables $tables = $this->wpdb->get_results(" SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024, 2) as size_mb, table_rows FROM information_schema.TABLES WHERE table_schema = DATABASE() ORDER BY (data_length + index_length) DESC LIMIT 10 "); foreach ($tables as $table) { $metrics['database']['tables'][] = [ 'name' => $table->table_name, 'size_mb' => $table->size_mb, 'rows' => $table->table_rows ]; } // Get database growth (using estimations from previous reports or calculating based on wp_options) $db_size_history = get_option('jvb_db_size_history', []); $current_size = $metrics['database']['size']; // Store current size to history $db_size_history[date('Y-m-d')] = $current_size; if (count($db_size_history) > 30) { // Keep only last 30 days $db_size_history = array_slice($db_size_history, -30, 30, true); } update_option('jvb_db_size_history', $db_size_history); // Calculate growth rates if (count($db_size_history) > 1) { $dates = array_keys($db_size_history); $sizes = array_values($db_size_history); // Daily growth (if we have yesterday's data) $yesterday_idx = array_search(date('Y-m-d', strtotime('-1 day')), $dates); if ($yesterday_idx !== false) { $yesterday_size = $sizes[$yesterday_idx]; $daily_growth = $current_size - $yesterday_size; $metrics['database']['growth']['daily'] = round($daily_growth, 2); $metrics['database']['growth']['daily_percent'] = round(($daily_growth / $yesterday_size) * 100, 2); } // Weekly growth $week_ago_idx = array_search(date('Y-m-d', strtotime('-7 days')), $dates); if ($week_ago_idx !== false) { $week_ago_size = $sizes[$week_ago_idx]; $weekly_growth = $current_size - $week_ago_size; $metrics['database']['growth']['weekly'] = round($weekly_growth, 2); $metrics['database']['growth']['weekly_percent'] = round(($weekly_growth / $week_ago_size) * 100, 2); } // Monthly growth (estimate based on available data) if (count($db_size_history) >= 7) { $oldest_date = min(array_keys($db_size_history)); $oldest_size = $db_size_history[$oldest_date]; $days_diff = (strtotime('now') - strtotime($oldest_date)) / 86400; if ($days_diff > 0) { $growth_per_day = ($current_size - $oldest_size) / $days_diff; $projected_monthly = $growth_per_day * 30; $metrics['database']['growth']['monthly_projected'] = round($projected_monthly, 2); $metrics['database']['growth']['monthly_percent_projected'] = round(($projected_monthly / $current_size) * 100, 2); } } } // Get media storage stats $upload_dir = wp_upload_dir(); $upload_base = $upload_dir['basedir']; if (function_exists('exec') && !in_array('exec', explode(',', ini_get('disable_functions')))) { // Use system du command if available for more accurate results exec("du -sk {$upload_base}", $output); if (!empty($output)) { // Extract size in KB and convert to MB $size_parts = explode("\t", $output[0]); $media_size_kb = (int)$size_parts[0]; $metrics['media']['total_size'] = round($media_size_kb / 1024, 2); } // Count files by type exec("find {$upload_base} -type f | grep -c '\\.jpg", $jpg_count); exec("find {$upload_base} -type f | grep -c '\\.png", $png_count); exec("find {$upload_base} -type f | grep -c '\\.webp", $webp_count); exec("find {$upload_base} -type f | grep -c '\\.gif", $gif_count); $metrics['media']['by_type']['jpg'] = !empty($jpg_count) ? (int)$jpg_count[0] : 0; $metrics['media']['by_type']['png'] = !empty($png_count) ? (int)$png_count[0] : 0; $metrics['media']['by_type']['webp'] = !empty($webp_count) ? (int)$webp_count[0] : 0; $metrics['media']['by_type']['gif'] = !empty($gif_count) ? (int)$gif_count[0] : 0; $metrics['media']['file_count'] = array_sum($metrics['media']['by_type']); } else { // Fallback to database count if exec is not available $attachment_count = $this->wpdb->get_var(" SELECT COUNT(*) FROM {$this->wpdb->posts} WHERE post_type = 'attachment' "); $metrics['media']['file_count'] = (int)$attachment_count; // Estimate size based on average attachment size and count // This is just a rough estimate $metrics['media']['total_size'] = round($attachment_count * 0.5, 2); // Assume 0.5MB average per file } // Get cache statistics // This is specific to your caching solution - this is a simple example for WP Object Cache if (wp_using_ext_object_cache()) { global $wp_object_cache; if (is_object($wp_object_cache) && method_exists($wp_object_cache, 'getStats')) { $cache_stats = $wp_object_cache->getStats(); if (!empty($cache_stats)) { $hits = $cache_stats['get'] ?? 0; $misses = $cache_stats['misses'] ?? 0; $total = $hits + $misses; if ($total > 0) { $metrics['cache']['hit_rate'] = round(($hits / $total) * 100, 2); } $metrics['cache']['size'] = round(($cache_stats['bytes'] ?? 0) / 1024 / 1024, 2); } } } return $metrics; } /** * Gather notification metrics * @return array */ protected function gatherNotificationMetrics():array { $metrics = [ 'total_sent' => 0, 'by_type' => [], 'read_rate' => 0, 'action_rate' => 0, 'most_active_users' => [] ]; $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours')); $notification_table = $this->wpdb->prefix . BASE . 'notifications_archive'; $notification_meta = $this->wpdb->prefix . BASE . 'notifications_archive_meta'; // Get total sent in last 24 hours $metrics['total_sent'] = $this->wpdb->get_var($this->wpdb->prepare(" SELECT COUNT(*) FROM {$notification_table} WHERE post_date > %s ", $yesterday)); // Get counts by notification type $by_type = $this->wpdb->get_results($this->wpdb->prepare(" SELECT meta_value as notification_type, COUNT(*) as count FROM {$notification_meta} m JOIN {$notification_table} n ON m.notification_id = n.ID WHERE meta_key = 'notification_type' AND n.post_date > %s GROUP BY meta_value ORDER BY count DESC ", $yesterday)); foreach ($by_type as $type) { $metrics['by_type'][$type->notification_type] = $type->count; } // Calculate read rates $read_stats = $this->wpdb->get_row($this->wpdb->prepare(" SELECT COUNT(*) as total, SUM(CASE WHEN m.meta_value = '1' THEN 1 ELSE 0 END) as read_count FROM {$notification_meta} m JOIN {$notification_table} n ON m.notification_id = n.ID WHERE m.meta_key = 'is_read' AND n.post_date > %s ", date('Y-m-d H:i:s', strtotime('-7 days')))); if ($read_stats && $read_stats->total > 0) { $metrics['read_rate'] = round(($read_stats->read_count / $read_stats->total) * 100, 2); } // Calculate action rates (notifications that led to clicks/actions) $action_stats = $this->wpdb->get_row($this->wpdb->prepare(" SELECT COUNT(*) as total, SUM(CASE WHEN m.meta_value = '1' THEN 1 ELSE 0 END) as action_count FROM {$notification_meta} m JOIN {$notification_table} n ON m.notification_id = n.ID WHERE m.meta_key = 'actioned' AND n.post_date > %s ", date('Y-m-d H:i:s', strtotime('-7 days')))); if ($action_stats && $action_stats->total > 0) { $metrics['action_rate'] = round(($action_stats->action_count / $action_stats->total) * 100, 2); } // Get users with most notifications $most_notified = $this->wpdb->get_results($this->wpdb->prepare(" SELECT post_author as user_id, (SELECT user_nicename FROM {$this->wpdb->users} WHERE ID = post_author) as username, COUNT(*) as notification_count FROM {$notification_table} WHERE post_date > %s GROUP BY post_author ORDER BY notification_count DESC LIMIT 10 ", date('Y-m-d H:i:s', strtotime('-7 days')))); foreach ($most_notified as $user) { $metrics['most_active_users'][] = [ 'user_id' => $user->user_id, 'username' => $user->username, 'notification_count' => $user->notification_count ]; } return $metrics; } /** * Format the daily report into an HTML email * @param array $error_stats * @param array $queue_stats * @param array $user_metrics * @param array $content_metrics * @param array $system_health * @param array $notification_metrics * @param array $user_retention * @param array $favourite_metrics * * @return string */ protected function formatDailyReport( array $error_stats, array $queue_stats, array $user_metrics, array $content_metrics, array $system_health, array $notification_metrics, array $user_retention, array $favourite_metrics ):string { $date = date('Y-m-d'); $css = $this->getEmailCSS(); $html = "
"; $html .= "{$date}
Total Logins: {$user_metrics['logins']['total']}
"; if (!empty($user_metrics['logins']['by_role'])) { $html .= "| Period | Count |
|---|---|
| {$label} | {$count} |
Total New Users: {$user_metrics['registrations']['total']}
"; if (!empty($user_metrics['registrations']['by_role'])) { $html .= "Total Approvals: {$user_metrics['approvals']['total']}
"; $html .= "Total New Shops: {$user_metrics['new_shops_count']}
"; $html .= "Total Uploads: {$content_metrics['total_uploads']}
"; if (!empty($content_metrics['uploads'])) { $html .= "| Taxonomy | New Terms | Examples |
|---|---|---|
| {$data['label']} | "; $html .= "{$data['count']} | "; $html .= "{$example_text} | "; $html .= "
New Favourites (24h): {$favourite_metrics['favourites_last_24h']}
"; $html .= "Total Favourites: {$favourite_metrics['total_favourites']}
"; // Favourites by content type if (!empty($favourite_metrics['by_content_type'])) { $html .= "Total Size: {$system_health['database']['size']} MB
"; // Database growth if (!empty($system_health['database']['growth'])) { $html .= "| Period | Growth (MB) | Percentage |
|---|---|---|
| Daily | {$system_health['database']['growth']['daily']} MB | {$system_health['database']['growth']['daily_percent']}% |
| Weekly | {$system_health['database']['growth']['weekly']} MB | {$system_health['database']['growth']['weekly_percent']}% |
| Monthly (Est.) | {$system_health['database']['growth']['monthly_projected']} MB | {$system_health['database']['growth']['monthly_percent_projected']}% |
| Table | Size (MB) | Rows |
|---|---|---|
| {$table['name']} | {$table['size_mb']} | {$table['rows']} |
Total Size: {$system_health['media']['total_size']} MB
"; $html .= "Total Files: {$system_health['media']['file_count']}
"; // Files by type if (!empty($system_health['media']['by_type'])) { $html .= "| Type | Count |
|---|---|
| .{$type} | {$count} |
Hit Rate: {$system_health['cache']['hit_rate']}%
"; $html .= "Cache Size: {$system_health['cache']['size']} MB
"; $html .= "Total Sent (24h): {$notification_metrics['total_sent']}
"; $html .= "Read Rate: {$notification_metrics['read_rate']}%
"; $html .= "Action Rate: {$notification_metrics['action_rate']}%
"; // Notifications by type if (!empty($notification_metrics['by_type'])) { $html .= "| Type | Count |
|---|---|
| " . ucfirst($type_display) . " | {$count} |
| User | Notifications |
|---|---|
| {$user['username']} | {$user['notification_count']} |
Total Errors (24h): {$error_stats['total_errors']}
"; // Errors by severity if (!empty($error_stats['by_severity'])) { $html .= "| Severity | Count |
|---|---|
| " . ucfirst($severity) . " | {$count} |
| Component | Count |
|---|---|
| " . ucfirst($component) . " | {$count} |
| Error Type | Component | Count |
|---|---|---|
| {$error->error_type} | {$error->component} | {$error->count} |
| Error Type | Component | Time |
|---|---|---|
| {$error->error_type} | {$error->component} | {$time} |
| Operation Type | Count |
|---|---|
| {$type} | {$count} |
| Priority | Count |
|---|---|
| " . ucfirst($priority) . " | {$count} |
ID: {$queue_stats['oldest_pending']['id']}
"; $html .= "Type: {$queue_stats['oldest_pending']['type']}
"; $html .= "Created: {$queue_stats['oldest_pending']['created_at']}
"; $html .= "Age: {$queue_stats['oldest_pending']['age_hours']} hours
"; } // Recent failures if (!empty($queue_stats['recent_failures'])) { $html .= "| ID | Type | Error |
|---|---|---|
| {$failure->id} | {$failure->type} | {$failure->error_message} |
| Operation Type | Average Time | Sample Size |
|---|---|---|
| {$type} | {$formatted_time} | {$data['samples']} |