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 .= "
"; $html .= "

edmonton.ink System Report

{$date}

"; // Executive Summary $html .= "
"; $html .= "

Executive Summary

"; $html .= "
"; $html .= "
New Users{$user_metrics['registrations']['total']}
"; $html .= "
Active Users (DAU){$user_retention['active_periods']['daily']}
"; $html .= "
Content Uploads{$content_metrics['total_uploads']}
"; $html .= "
New Favourites{$favourite_metrics['favourites_last_24h']}
"; $html .= "
Notifications{$notification_metrics['total_sent']}
"; $html .= "
Errors{$error_stats['total_errors']}
"; $html .= "
"; $html .= "
"; // User Activity Section $html .= "
"; $html .= "

User Activity (Last 24 Hours)

"; // Login statistics $html .= "
"; $html .= "

Login Activity

"; $html .= "

Total Logins: {$user_metrics['logins']['total']}

"; if (!empty($user_metrics['logins']['by_role'])) { $html .= "
    "; foreach ($user_metrics['logins']['by_role'] as $role => $count) { $role_name = ucfirst($role); $html .= "
  • {$role_name}s: {$count}
  • "; } $html .= "
"; } $html .= "
"; // Retention metrics $html .= "
"; $html .= "

User Retention

"; $html .= "
"; $html .= "
Daily Active{$user_retention['active_periods']['daily']}
"; $html .= "
Weekly Active{$user_retention['active_periods']['weekly']}
"; $html .= "
Monthly Active{$user_retention['active_periods']['monthly']}
"; $html .= "
"; // Inactive user metrics $html .= "

Inactive Users

"; $html .= ""; $html .= ""; foreach ($user_retention['inactive_counts'] as $key => $count) { $label = str_replace(['inactive_', 'mo', 'yr', '_'], ['', ' months', ' year', '-'], $key); $html .= ""; } $html .= "
PeriodCount
{$label}{$count}
"; $html .= "
"; // Registration statistics $html .= "
"; $html .= "

New Registrations

"; $html .= "

Total New Users: {$user_metrics['registrations']['total']}

"; if (!empty($user_metrics['registrations']['by_role'])) { $html .= "
    "; foreach ($user_metrics['registrations']['by_role'] as $role => $count) { $role_name = ucfirst($role); $html .= "
  • {$role_name}s: {$count}
  • "; } $html .= "
"; } $html .= "
"; // Approvals if ($user_metrics['approvals']['total'] > 0) { $html .= "
"; $html .= "

User Approvals

"; $html .= "

Total Approvals: {$user_metrics['approvals']['total']}

"; $html .= "
    "; if (!empty($user_metrics['approvals']['artists'])) { $html .= "
  • Artists: " . count($user_metrics['approvals']['artists']) . " (" . implode(', ', $user_metrics['approvals']['artists']) . ")
  • "; } if (!empty($user_metrics['approvals']['partners'])) { $html .= "
  • Partners: " . count($user_metrics['approvals']['partners']) . " (" . implode(', ', $user_metrics['approvals']['partners']) . ")
  • "; } $html .= "
"; $html .= "
"; } // New Shops if (!empty($user_metrics['new_shops'])) { $html .= "
"; $html .= "

New Shops

"; $html .= "

Total New Shops: {$user_metrics['new_shops_count']}

"; $html .= "
    "; foreach ($user_metrics['new_shops'] as $shop) { $html .= "
  • {$shop['name']} (ID: {$shop['id']})
  • "; } $html .= "
"; $html .= "
"; } $html .= "
"; // Content Metrics Section $html .= "
"; $html .= "

Content Activity

"; // Content uploads $html .= "
"; $html .= "

Content Uploads

"; $html .= "

Total Uploads: {$content_metrics['total_uploads']}

"; if (!empty($content_metrics['uploads'])) { $html .= "
"; foreach ($content_metrics['uploads'] as $type => $count) { $type_name = ucfirst($type); $percentage = ($content_metrics['total_uploads'] > 0) ? round(($count / $content_metrics['total_uploads']) * 100) : 0; $html .= "
"; $html .= "{$type_name}s"; $html .= "
"; $html .= "
"; $html .= "{$count}"; $html .= "
"; $html .= "
"; } $html .= "
"; } // Top uploaders if (!empty($content_metrics['by_user'])) { $html .= "

Top Content Contributors

"; $html .= "
    "; foreach (array_slice($content_metrics['by_user'], 0, 5) as $user_data) { $html .= "
  1. {$user_data['username']}: {$user_data['total']} items ("; $types = []; foreach ($user_data['types'] as $type => $count) { $types[] = ucfirst($type) . "s: {$count}"; } $html .= implode(', ', $types) . ")
  2. "; } $html .= "
"; } $html .= "
"; // Taxonomy growth if (!empty($content_metrics['taxonomy_growth'])) { $html .= "
"; $html .= "

Taxonomy Growth

"; $html .= ""; $html .= ""; foreach ($content_metrics['taxonomy_growth'] as $taxonomy => $data) { $examples = array_slice(array_map(function ($term) { return $term['name']; }, $data['terms']), 0, 3); $example_text = !empty($examples) ? implode(', ', $examples) : 'None'; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; } $html .= "
TaxonomyNew TermsExamples
{$data['label']}{$data['count']}{$example_text}
"; $html .= "
"; } // Favourite metrics $html .= "
"; $html .= "

Favourite Activity

"; $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 .= "

Favourites by Content Type

"; $html .= "
"; foreach ($favourite_metrics['by_content_type'] as $type => $count) { $percentage = ($favourite_metrics['total_favourites'] > 0) ? round(($count / $favourite_metrics['total_favourites']) * 100) : 0; $html .= "
"; $html .= "" . ucfirst($type) . "s"; $html .= "
"; $html .= "
"; $html .= "{$count}"; $html .= "
"; $html .= "
"; } $html .= "
"; } // Top artists by favourites if (!empty($favourite_metrics['top_artists'])) { $html .= "

Top Artists by Favourites

"; $html .= "
    "; foreach (array_slice($favourite_metrics['top_artists'], 0, 5) as $artist) { $html .= "
  1. {$artist['name']}: {$artist['favourite_count']} favourites
  2. "; } $html .= "
"; } $html .= "
"; $html .= "
"; // System Health Section $html .= "
"; $html .= "

System Health

"; // Database metrics $html .= "
"; $html .= "

Database

"; $html .= "

Total Size: {$system_health['database']['size']} MB

"; // Database growth if (!empty($system_health['database']['growth'])) { $html .= "

Growth

"; $html .= ""; $html .= ""; if (isset($system_health['database']['growth']['daily'])) { $html .= ""; } if (isset($system_health['database']['growth']['weekly'])) { $html .= ""; } if (isset($system_health['database']['growth']['monthly_projected'])) { $html .= ""; } $html .= "
PeriodGrowth (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']}%
"; } // Largest tables if (!empty($system_health['database']['tables'])) { $html .= "

Largest Tables

"; $html .= ""; $html .= ""; foreach (array_slice($system_health['database']['tables'], 0, 5) as $table) { $html .= ""; } $html .= "
TableSize (MB)Rows
{$table['name']}{$table['size_mb']}{$table['rows']}
"; } $html .= "
"; // Media storage $html .= "
"; $html .= "

Media Storage

"; $html .= "

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 .= "

Files by Type

"; $html .= ""; $html .= ""; foreach ($system_health['media']['by_type'] as $type => $count) { $html .= ""; } $html .= "
TypeCount
.{$type}{$count}
"; } $html .= "
"; // Cache metrics if ($system_health['cache']['hit_rate'] > 0) { $html .= "
"; $html .= "

Cache Performance

"; $html .= "

Hit Rate: {$system_health['cache']['hit_rate']}%

"; $html .= "

Cache Size: {$system_health['cache']['size']} MB

"; $html .= "
"; } $html .= "
"; // Notification Metrics Section $html .= "
"; $html .= "

Notification Activity

"; $html .= "
"; $html .= "

Notification Stats

"; $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 .= "

Notifications by Type

"; $html .= ""; $html .= ""; foreach ($notification_metrics['by_type'] as $type => $count) { $type_display = str_replace('_', ' ', $type); $html .= ""; } $html .= "
TypeCount
" . ucfirst($type_display) . "{$count}
"; } $html .= "
"; // Most active users if (!empty($notification_metrics['most_active_users'])) { $html .= "
"; $html .= "

Most Notified Users

"; $html .= ""; $html .= ""; foreach (array_slice($notification_metrics['most_active_users'], 0, 5) as $user) { $html .= ""; } $html .= "
UserNotifications
{$user['username']}{$user['notification_count']}
"; $html .= "
"; } $html .= "
"; // Error Statistics Section $html .= "
"; $html .= "

Error Statistics

"; $html .= "
"; $html .= "

Error Overview

"; $html .= "

Total Errors (24h): {$error_stats['total_errors']}

"; // Errors by severity if (!empty($error_stats['by_severity'])) { $html .= "

Errors by Severity

"; $html .= ""; $html .= ""; foreach ($error_stats['by_severity'] as $severity => $count) { $html .= ""; } $html .= "
SeverityCount
" . ucfirst($severity) . "{$count}
"; } // Errors by component if (!empty($error_stats['by_component'])) { $html .= "

Errors by Component

"; $html .= ""; $html .= ""; foreach ($error_stats['by_component'] as $component => $count) { $html .= ""; } $html .= "
ComponentCount
" . ucfirst($component) . "{$count}
"; } // Most frequent errors if (!empty($error_stats['frequent'])) { $html .= "

Most Frequent Errors

"; $html .= ""; $html .= ""; foreach (array_slice($error_stats['frequent'], 0, 5) as $error) { $html .= ""; } $html .= "
Error TypeComponentCount
{$error->error_type}{$error->component}{$error->count}
"; } // Critical errors if (!empty($error_stats['critical'])) { $html .= "

Recent Critical Errors

"; $html .= ""; $html .= ""; foreach ($error_stats['critical'] as $error) { $time = date('H:i:s', strtotime($error->created_at)); $html .= ""; } $html .= "
Error TypeComponentTime
{$error->error_type}{$error->component}{$time}
"; } $html .= "
"; $html .= "
"; // Queue Statistics Section $html .= "
"; $html .= "

Queue Statistics

"; $html .= "
"; $html .= "

Queue Overview

"; // Queue by status if (!empty($queue_stats['by_status'])) { $html .= "
"; foreach ($queue_stats['by_status'] as $status => $data) { $html .= "
"; $html .= "" . ucfirst($status) . ""; $html .= "{$data['count']}"; $html .= "{$data['operations']} operations"; $html .= "
"; } $html .= "
"; } // Queue by type if (!empty($queue_stats['by_type'])) { $html .= "

Queue by Operation Type

"; $html .= ""; $html .= ""; foreach ($queue_stats['by_type'] as $type => $count) { $html .= ""; } $html .= "
Operation TypeCount
{$type}{$count}
"; } // Pending by priority if (!empty($queue_stats['pending_by_priority'])) { $html .= "

Pending Operations by Priority

"; $html .= ""; $html .= ""; foreach ($queue_stats['pending_by_priority'] as $priority => $count) { $html .= ""; } $html .= "
PriorityCount
" . ucfirst($priority) . "{$count}
"; } // Oldest pending operation if (!empty($queue_stats['oldest_pending'])) { $html .= "

Oldest Pending Operation

"; $html .= "

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 .= "

Recent Failed Operations

"; $html .= ""; $html .= ""; foreach ($queue_stats['recent_failures'] as $failure) { $html .= ""; } $html .= "
IDTypeError
{$failure->id}{$failure->type}{$failure->error_message}
"; } // Average processing times if (!empty($queue_stats['avg_processing_times'])) { $html .= "

Average Processing Times

"; $html .= ""; $html .= ""; foreach ($queue_stats['avg_processing_times'] as $type => $data) { $formatted_time = $this->formatSeconds($data['seconds']); $html .= ""; } $html .= "
Operation TypeAverage TimeSample Size
{$type}{$formatted_time}{$data['samples']}
"; } $html .= "
"; $html .= "
"; // Footer $html .= ""; $html .= "
"; return $html; } /** * Format seconds into a human-readable string * @param float $seconds * * @return string */ protected function formatSeconds(float $seconds):string { if ($seconds < 1) { return round($seconds * 1000) . " ms"; } elseif ($seconds < 60) { return round($seconds, 2) . " seconds"; } elseif ($seconds < 3600) { $minutes = floor($seconds / 60); $remaining_seconds = $seconds % 60; return "{$minutes}m " . round($remaining_seconds) . "s"; } else { $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); return "{$hours}h {$minutes}m"; } } /** * Send email to admin * @param string $subject * @param string $content * * @return void */ protected function sendAdminEmail(string $subject, string $content):void { $admin_email = get_option('admin_email'); JVB()->email()->sendEmail($admin_email, $subject, $content); // Also send to any additional configured recipients $additional_recipients = get_option('jvb_report_recipients', []); if (!empty($additional_recipients)) { foreach ($additional_recipients as $recipient) { JVB()->email()->sendEmail($recipient, $subject, $content); } } } /** * Get CSS styles for email * @return string */ protected function getEmailCSS():string { return ' body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f5f5f5; margin: 0; padding: 0; } .container { max-width: 900px; margin: 0 auto; background: #fff; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eee; } header h1 { color: #FF0080; margin: 0; font-size: 28px; } header .date { font-size: 16px; color: #777; } h2 { color: #333; padding-bottom: 10px; border-bottom: 2px solid #FF0080; margin-top: 40px; } h3 { color: #444; margin-top: 25px; margin-bottom: 15px; } h4 { color: #555; margin-top: 20px; margin-bottom: 10px; font-weight: 500; } section { margin-bottom: 40px; } .card { background: #fff; border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .summary { background-color: #f8f8f8; padding: 15px; margin: 20px 0; border-left: 4px solid #FF0080; } .summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px; } .summary-item { text-align: center; padding: 10px; background: white; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .summary-item .label { display: block; font-size: 14px; color: #666; margin-bottom: 5px; } .summary-item .value { display: block; font-size: 24px; font-weight: bold; color: #FF0080; } .data-table { width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 14px; } .data-table th { background-color: #f5f5f5; text-align: left; padding: 8px; border-bottom: 2px solid #ddd; } .data-table td { padding: 8px; border-bottom: 1px solid #ddd; } .data-table tr:nth-child(even) { background-color: #f9f9f9; } .chart-container { margin: 20px 0; } .chart-row { display: flex; align-items: center; margin-bottom: 8px; } .chart-label { width: 120px; text-align: right; padding-right: 10px; font-size: 14px; } .chart-bar-container { flex: 1; height: 25px; background-color: #eee; border-radius: 3px; position: relative; overflow: hidden; } .chart-bar { height: 100%; background-color: #FF0080; border-radius: 3px 0 0 3px; } .chart-value { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); color: #333; font-weight: bold; font-size: 14px; text-shadow: 0 0 5px white; } .stat-group { display: flex; flex-wrap: wrap; gap: 15px; margin: 15px 0; } .stat-item { background: #f8f8f8; flex: 1; min-width: 120px; padding: 10px; text-align: center; border-radius: 3px; } .stat-item .label { font-size: 14px; color: #666; display: block; } .stat-item .value { font-size: 20px; font-weight: bold; display: block; margin: 5px 0; color: #333; } .stat-item .subtext { font-size: 12px; color: #999; display: block; } .top-list { margin: 15px 0; padding-left: 20px; } .top-list li { margin-bottom: 8px; } footer { text-align: center; margin-top: 30px; padding-top: 15px; border-top: 1px solid #eee; color: #777; font-size: 14px; } '; } /** * Add recipient to the report email list * @param string $email * * @return bool */ public function addReportRecipient(string $email):bool { if (!is_email($email)) { return false; } $current_recipients = get_option('jvb_report_recipients', []); if (!in_array($email, $current_recipients)) { $current_recipients[] = $email; update_option('jvb_report_recipients', $current_recipients); return true; } return false; } /** * Remove recipient from the report email list * @param string $email * * @return bool */ public function removeReportRecipient(string $email):bool { $current_recipients = get_option('jvb_report_recipients', []); $key = array_search($email, $current_recipients); if ($key !== false) { unset($current_recipients[$key]); update_option('jvb_report_recipients', array_values($current_recipients)); return true; } return false; } /** * Change report frequency * @param string $interval * * @return bool */ public function setReportInterval(string $interval):bool { if (!in_array($interval, ['daily', 'weekly'])) { return false; } // Clear existing schedule wp_clear_scheduled_hook('jvb_generate_daily_report'); // Set new interval $this->report_interval = $interval; // Schedule new report if ($interval === 'daily') { wp_schedule_event(strtotime('tomorrow 01:00:00'), 'daily', 'jvb_generate_daily_report'); } else { wp_schedule_event(strtotime('next monday 01:00:00'), 'weekly', 'jvb_generate_daily_report'); } return true; } /** * Generate an on-demand report * @return bool */ public function generateOnDemandReport():bool { // Collect metrics like in the daily report $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 report $subject = "[edmonton.ink] On-Demand System Report - " . date('Y-m-d H:i'); $content = $this->formatDailyReport( $error_stats, $queue_stats, $user_metrics, $content_metrics, $system_health, $notification_metrics, $user_retention, $favourite_metrics ); // Send to admin only $admin_email = get_option('admin_email'); return JVB()->email()->sendEmail($admin_email, $subject, $content); } //List report: /** * @return bool */ public function rebuildAllListStats():bool { $items_table = $this->wpdb->prefix . BASE . 'list_items'; $stats_table = $this->wpdb->prefix . BASE . 'list_stats'; // Start transaction $this->wpdb->query('START TRANSACTION'); try { // Get distinct item types and IDs $distinct_items = $this->wpdb->get_results(" SELECT DISTINCT item_type, item_id FROM {$items_table} "); // Prepare items for batch update $items_to_update = []; foreach ($distinct_items as $item) { $items_to_update[] = [$item->item_type, $item->item_id]; } // Process in reasonable batches (500 items per batch) $batches = array_chunk($items_to_update, 500); foreach ($batches as $batch) { $this->batchUpdateListStats($batch); } // Commit transaction $this->wpdb->query('COMMIT'); // Log success JVB()->error()->log( 'favourites', 'Successfully rebuilt all list statistics', ['items_processed' => count($distinct_items)], 'info' ); return true; } catch (Exception $e) { // Rollback on error $this->wpdb->query('ROLLBACK'); JVB()->error()->log( 'favourites', 'Error rebuilding list statistics: ' . $e->getMessage(), [], 'error' ); return false; } } /** * Update list statistics for multiple items at once * * @param array $items Array of [type, id] pairs */ protected function batchUpdateListStats(array $items):void { if (empty($items)) { return; } $stats_table = $this->wpdb->prefix . BASE . 'list_stats'; $items_table = $this->wpdb->prefix . BASE . 'list_items'; // Get counts for all items at once $query_parts = []; $query_params = []; foreach ($items as $item_data) { list($type, $id) = $item_data; $query_parts[] = "(item_type = %s AND item_id = %d)"; $query_params[] = $type; $query_params[] = $id; } $count_query = "SELECT item_type, item_id, COUNT(DISTINCT list_id) as list_count FROM {$items_table} WHERE " . implode(' OR ', $query_parts) . " GROUP BY item_type, item_id"; $results = $this->wpdb->get_results($this->wpdb->prepare($count_query, $query_params)); if (empty($results)) { return; } $current_time = current_time('mysql'); $existing_items = []; // Find existing records $existing_query_parts = []; $existing_query_params = []; foreach ($results as $result) { $existing_query_parts[] = "(item_type = %s AND item_id = %d)"; $existing_query_params[] = $result->item_type; $existing_query_params[] = $result->item_id; } $existing_query = "SELECT item_type, item_id FROM {$stats_table} WHERE " . implode(' OR ', $existing_query_parts); $existing_results = $this->wpdb->get_results($this->wpdb->prepare($existing_query, $existing_query_params)); foreach ($existing_results as $existing) { $existing_items["{$existing->item_type}-{$existing->item_id}"] = true; } // Prepare update and insert statement formats $update_query = "UPDATE {$stats_table} SET list_count = %d, last_added = %s WHERE item_type = %s AND item_id = %d"; $insert_query = "INSERT INTO {$stats_table} (item_type, item_id, list_count, last_added) VALUES (%s, %d, %d, %s)"; // Prepare batched updates and inserts $updates = []; $inserts = []; foreach ($results as $result) { $key = "{$result->item_type}-{$result->item_id}"; if (isset($existing_items[$key])) { $updates[] = $this->wpdb->prepare( $update_query, $result->list_count, $current_time, $result->item_type, $result->item_id ); } else { $inserts[] = $this->wpdb->prepare( "(%s, %d, %d, %s)", $result->item_type, $result->item_id, $result->list_count, $current_time ); } } // Execute updates in batches if (!empty($updates)) { foreach ($updates as $update_sql) { $this->wpdb->query($update_sql); } } // Execute inserts in one go if possible if (!empty($inserts)) { $multi_insert = "INSERT INTO {$stats_table} (item_type, item_id, list_count, last_added) VALUES " . implode(', ', $inserts); $this->wpdb->query($multi_insert); } } }