create_logs_tables_for_all_sites(); } // Initialize settings manager and create settings table for all sites. if ( class_exists( 'Gregius_Data_Settings_Manager' ) ) { $settings_manager = new Gregius_Data_Settings_Manager(); $settings_manager->create_settings_tables_for_all_sites(); } // Initialize schema manager and create sync metadata tables for all sites. if ( class_exists( 'Gregius_Data_Schema_Manager' ) ) { $schema_manager = new Gregius_Data_Schema_Manager(); $schema_manager->create_sync_metadata_tables_for_all_sites(); } // Initialize search settings. if ( function_exists( 'gregius_data_init_search_settings' ) ) { gregius_data_init_search_settings(); } // Initialize default content sync settings. self::init_default_sync_settings(); // Add scheduled events for connection health checks and sync validation. self::schedule_monitoring_events(); // Add scheduled event for daily log retention purge (site-scoped in multisite). self::schedule_log_retention_events(); // Seed factory prompt terms and defaults. self::ensure_prompt_infrastructure(); self::ensure_prompt_type_terms(); self::seed_default_prompts(); } /** * Ensure the prompt post type and taxonomy are registered. * * The seeder runs during plugin activation, before the `init` hook has * fired, so the taxonomy is not yet registered. Registering it here makes * term creation/assignment safe regardless of activation timing. * * @since 1.0.0 */ private static function ensure_prompt_infrastructure() { if ( ! class_exists( 'Gregius_Data_Prompt' ) ) { return; } $prompt = new Gregius_Data_Prompt(); if ( ! post_type_exists( Gregius_Data_Prompt::POST_TYPE ) ) { $prompt->register_post_type(); } if ( ! taxonomy_exists( Gregius_Data_Prompt::TAXONOMY ) ) { $prompt->register_prompt_type_taxonomy(); } } /** * Ensure required prompt type terms exist. * * @since 1.0.0 */ private static function ensure_prompt_type_terms() { if ( get_option( 'gregius_prompt_type_terms_seeded' ) ) { return; } self::ensure_prompt_infrastructure(); $terms = array( 'system' => __( 'System Prompt', 'gregius-data' ), 'security' => __( 'Security Prompt', 'gregius-data' ), 'evaluation' => __( 'Evaluation Prompt', 'gregius-data' ), ); $all_present = true; foreach ( $terms as $slug => $name ) { if ( ! get_term_by( 'slug', $slug, Gregius_Data_Prompt::TAXONOMY ) ) { $inserted = wp_insert_term( $name, Gregius_Data_Prompt::TAXONOMY, array( 'slug' => $slug, ) ); if ( is_wp_error( $inserted ) ) { $all_present = false; continue; } } // Only count the slug as done once it is confirmed to exist. if ( ! get_term_by( 'slug', $slug, Gregius_Data_Prompt::TAXONOMY ) ) { $all_present = false; } } if ( $all_present ) { update_option( 'gregius_prompt_type_terms_seeded', true ); } } /** * Seed factory prompts for system answer generation and security gatekeeping. * * @since 1.0.0 */ private static function seed_default_prompts() { self::seed_prompt_post( 'gregius_data_default_prompt_seeded', __( 'System Default', 'gregius-data' ), 'system', 'You are a helpful assistant for a WordPress website. Current date: {{date}}. Current time: {{time}}. IMPORTANT: Answer questions using ONLY the provided context. Include inline citations using [Source N] markers that match the context labels (for example: [Source 1]). Keep writing natural and conversational.', true ); self::seed_prompt_post( 'gregius_data_security_prompt_seeded', __( 'Security Default', 'gregius-data' ), 'security', 'You are a security gatekeeper for a WordPress site assistant. Review the user query and classify it as safe or unsafe. Mark unsafe when the query requests illegal, harmful, exploitative, violent, hateful, sexual, self-harm, malware, privacy invasion, credential theft, social engineering, or policy-evading instructions. Respond with this exact JSON shape only: {"status":"safe|unsafe","reason":"short reason"} Keep reason concise. Do not add extra keys or text.', true ); self::seed_prompt_post( 'gregius_data_evaluation_prompt_seeded', __( 'Evaluation Default', 'gregius-data' ), 'evaluation', 'You are a retrieval quality evaluator. The user\'s query and the retrieved context are provided in the state. Judge whether the retrieved context is sufficient to answer the query accurately and completely. Answer "yes" (sufficient) only when the context covers the query\'s subject with specific, supporting detail. Answer "no" (insufficient) when the context is too thin, off-topic, stale, or missing key facts needed for a grounded answer. Be strict: prefer "no" when the context does not clearly support a grounded answer.', true ); } /** * Seed a factory prompt post if it does not exist yet. * * @since 1.0.0 * @param string $option_name Site option used as idempotency guard. * @param string $title Prompt title. * @param string $prompt_type Prompt type term slug. * @param string $content Prompt content. * @param bool $selected Whether prompt is active by default. */ private static function seed_prompt_post( $option_name, $title, $prompt_type, $content, $selected ) { if ( get_option( $option_name ) ) { return; } if ( ! class_exists( 'Gregius_Data_Prompt' ) ) { return; } $post_id = wp_insert_post( array( 'post_type' => Gregius_Data_Prompt::POST_TYPE, 'post_status' => 'publish', 'post_author' => 1, 'post_title' => $title, 'post_content' => wp_kses_post( $content ), ), true ); if ( is_wp_error( $post_id ) ) { return; } $hash = hash( 'sha256', str_replace( "\r\n", "\n", trim( $content ) ) ); update_post_meta( $post_id, '_gregius_prompt_version', 1 ); update_post_meta( $post_id, '_gregius_prompt_status', 'published' ); update_post_meta( $post_id, '_gregius_prompt_hash', $hash ); update_post_meta( $post_id, '_gregius_prompt_notes', '' ); update_post_meta( $post_id, '_gregius_prompt_is_factory', '1' ); update_post_meta( $post_id, '_gregius_prompt_factory_type', sanitize_key( $prompt_type ) ); update_post_meta( $post_id, '_gregius_prompt_selected', $selected ? '1' : '' ); self::ensure_prompt_infrastructure(); wp_set_object_terms( $post_id, array( sanitize_key( $prompt_type ) ), Gregius_Data_Prompt::TAXONOMY, false ); // Only mark this prompt as seeded once the type term is actually // assigned, so a partial seed can be retried later. $assigned_terms = wp_get_object_terms( $post_id, Gregius_Data_Prompt::TAXONOMY, array( 'fields' => 'slugs' ) ); if ( ! is_wp_error( $assigned_terms ) && ! empty( $assigned_terms ) ) { update_option( $option_name, true ); } } /** * Ensure legacy prompts have a prompt type assigned. * * @since 1.0.0 */ private static function migrate_prompt_types() { if ( get_option( 'gregius_prompt_type_migrated' ) ) { return; } if ( ! class_exists( 'Gregius_Data_Prompt' ) || ! taxonomy_exists( Gregius_Data_Prompt::TAXONOMY ) ) { return; } $prompts = get_posts( array( 'post_type' => Gregius_Data_Prompt::POST_TYPE, 'post_status' => array( 'publish', 'draft' ), 'posts_per_page' => -1, 'fields' => 'ids', ) ); foreach ( $prompts as $prompt_id ) { // Factory prompts carry an authoritative type — never default them. if ( '1' === (string) get_post_meta( (int) $prompt_id, '_gregius_prompt_is_factory', true ) ) { continue; } $terms = wp_get_object_terms( (int) $prompt_id, Gregius_Data_Prompt::TAXONOMY, array( 'fields' => 'slugs' ) ); if ( is_wp_error( $terms ) || ! empty( $terms ) ) { continue; } wp_set_object_terms( (int) $prompt_id, array( 'system' ), Gregius_Data_Prompt::TAXONOMY, false ); } update_option( 'gregius_prompt_type_migrated', true ); } /** * Replace any baked-in date/time values in the system default prompt with * {{date}} / {{time}} placeholders so the resolver can inject live values. * * Runs once, guarded by a site option. */ private static function migrate_factory_prompt_to_placeholders() { if ( get_option( 'gregius_data_factory_prompt_placeholders_migrated' ) ) { return; } $posts = get_posts( array( 'post_type' => 'gregius_prompt', 'post_status' => 'publish', 'numberposts' => -1, ) ); foreach ( $posts as $post ) { if ( '1' !== (string) get_post_meta( $post->ID, '_gregius_prompt_is_factory', true ) ) { continue; } $content = $post->post_content; // Replace "Current date: ." and "Current time: ." // with their placeholder equivalents. $content = preg_replace( '/Current date: [^.]+\./', 'Current date: {{date}}.', $content ); $content = preg_replace( '/Current time: [^.]+\./', 'Current time: {{time}}.', $content ); if ( $content !== $post->post_content ) { wp_update_post( array( 'ID' => $post->ID, 'post_content' => wp_kses_post( $content ), ) ); $hash = hash( 'sha256', str_replace( "\r\n", "\n", trim( $content ) ) ); update_post_meta( $post->ID, '_gregius_prompt_hash', $hash ); } } update_option( 'gregius_data_factory_prompt_placeholders_migrated', true ); } /** * Backfill prompt type terms for factory prompts on existing installs. * * Repairs sites where the factory prompts were seeded before the * gregius_prompt_type taxonomy was registered (activation-time ordering), * leaving them without a system/security term. * * Runs once, guarded by a site option, on the init hook. * * @since 1.0.0 */ public static function maybe_backfill_prompt_types() { if ( get_option( 'gregius_prompt_type_backfill' ) ) { return; } if ( ! class_exists( 'Gregius_Data_Prompt' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::backfill_prompt_types_for_current_site(); restore_current_blog(); } return; } self::backfill_prompt_types_for_current_site(); } /** * Backfill prompt type terms for the current site. * * @since 1.0.0 */ private static function backfill_prompt_types_for_current_site() { self::ensure_prompt_infrastructure(); self::ensure_prompt_type_terms(); $prompts = get_posts( array( 'post_type' => Gregius_Data_Prompt::POST_TYPE, 'post_status' => array( 'publish', 'draft' ), 'posts_per_page' => -1, 'fields' => 'ids', ) ); foreach ( $prompts as $prompt_id ) { $terms = wp_get_object_terms( (int) $prompt_id, Gregius_Data_Prompt::TAXONOMY, array( 'fields' => 'slugs' ) ); if ( is_wp_error( $terms ) || ! empty( $terms ) ) { continue; } $prompt_type = self::resolve_factory_prompt_type( (int) $prompt_id ); if ( '' === $prompt_type ) { continue; } wp_set_object_terms( (int) $prompt_id, array( $prompt_type ), Gregius_Data_Prompt::TAXONOMY, false ); } update_option( 'gregius_prompt_type_backfill', true ); } /** * Determine the prompt type for a termless factory prompt. * * Prefers the stored factory-type meta; otherwise infers from the title * and content. Returns an empty string when the type cannot be determined. * * @since 1.0.0 * @param int $prompt_id Prompt post ID. * @return string 'system', 'security', or ''. */ private static function resolve_factory_prompt_type( $prompt_id ) { $factory_type = (string) get_post_meta( $prompt_id, '_gregius_prompt_factory_type', true ); if ( '' !== $factory_type ) { $factory_type = sanitize_key( $factory_type ); return in_array( $factory_type, array( 'system', 'security' ), true ) ? $factory_type : ''; } if ( '1' !== (string) get_post_meta( $prompt_id, '_gregius_prompt_is_factory', true ) ) { return ''; } $title = strtolower( (string) get_the_title( $prompt_id ) ); $content = strtolower( (string) get_post_field( 'post_content', $prompt_id ) ); if ( false !== strpos( $title, 'security' ) || false !== strpos( $content, 'security gatekeeper' ) || false !== strpos( $content, 'safe or unsafe' ) ) { return 'security'; } return 'system'; } /** * Migrate from tfidf-300 to hashingtf-murmur3-1024 as the sole internal * embedding model. * * Removes the retired tfidf-300 model, its PostgreSQL vector + vocabulary * tables, and retargets the search default to hashingtf. Runs once, * guarded by a site option. * * @since 1.0.0 */ public static function migrate_hashingtf() { if ( get_option( 'gregius_data_hashingtf_migrated' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_hashingtf_for_current_site(); restore_current_blog(); } return; } self::migrate_hashingtf_for_current_site(); } /** * Migrate the current site from tfidf-300 to hashingtf. * * @since 1.0.0 */ private static function migrate_hashingtf_for_current_site() { $settings_manager = new Gregius_Data_Settings_Manager(); $connections = method_exists( $settings_manager, 'get_all_connections' ) ? $settings_manager->get_all_connections() : array(); // 1. Remove the retired tfidf-300 model from the global registry. if ( class_exists( 'Gregius_Data_Model_Registry' ) ) { $registry = new Gregius_Data_Model_Registry(); $registry->delete_model( 'gregius-data', 'tfidf-300' ); } // 2. Strip tfidf-300 from every connection's active model list. foreach ( array_keys( (array) $connections ) as $connection_name ) { $active_models = $settings_manager->get_with_category( 'vectors', $connection_name, 'active_models', array() ); if ( ! is_array( $active_models ) || ! in_array( 'tfidf-300', $active_models, true ) ) { continue; } $active_models = array_values( array_filter( $active_models, static function ( $model_key ) { return 'tfidf-300' !== $model_key; } ) ); $settings_manager->set_with_category( 'vectors', $connection_name, 'active_models', $active_models, 'serialized' ); } // 3. Retarget the search embedding model default. $search_model = $settings_manager->get_with_category( 'search', GREGIUS_DATA_SEARCH_SETTINGS_CONNECTION, 'embedding_model', null ); if ( 'tfidf-300' === $search_model ) { $settings_manager->set_with_category_public( 'search', GREGIUS_DATA_SEARCH_SETTINGS_CONNECTION, 'embedding_model', 'hashingtf-murmur3-1024' ); } // 4. Remove retired tfidf / vector-processor options. foreach ( array( 'gregius_data_tfidf_300_processing', 'gregius_data_tfidf_300_start_time', 'gregius_data_tfidf_300_progress', 'gregius_data_simple_vectors_processing', 'gregius_data_simple_vectors_start_time' ) as $option ) { delete_option( $option ); } // 5. Drop retired PostgreSQL tables and re-apply the search function. $db = new Gregius_Data_DB(); foreach ( array_keys( (array) $connections ) as $connection_name ) { try { $conn = $db->get_connection( $connection_name ); if ( ! $conn ) { continue; } $conn->exec( 'DROP TABLE IF EXISTS wp_posts_tfidf_300' ); $conn->exec( 'DROP TABLE IF EXISTS wp_posts_vocabulary_cache' ); if ( class_exists( 'Gregius_Data_Search_Schema' ) ) { $search_schema = new Gregius_Data_Search_Schema(); $search_schema->create_search_function( $conn, $connection_name ); } } catch ( Exception $e ) { // PostgreSQL cleanup is best-effort; the schema can be re-run manually. } } update_option( 'gregius_data_hashingtf_migrated', true ); } /** * Rebuild the precomputed search-vector column in the site language and * re-apply the collapsed search function on existing installs. * * Repairs installs whose `search_vector_weighted` column was built in a * different language before the search-vector collapse landed. Runs * once, guarded by a site option. PostgreSQL (PDO) only; Supabase is * manual. * * @since 1.0.0 */ public static function migrate_search_vector_language() { if ( get_option( 'gregius_data_search_vector_language_migrated' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_search_vector_language_for_current_site(); restore_current_blog(); } return; } self::migrate_search_vector_language_for_current_site(); } /** * Rebuild the search-vector column and re-apply the search function for * the current site. * * @return void */ private static function migrate_search_vector_language_for_current_site() { $language = Gregius_Data_Search_Language::get_site_search_language(); $settings = new Gregius_Data_Settings_Manager(); $connections = method_exists( $settings, 'get_all_connections' ) ? $settings->get_all_connections() : array(); $db = new Gregius_Data_DB(); foreach ( array_keys( (array) $connections ) as $connection_name ) { try { $conn = $db->get_connection( $connection_name ); if ( ! $conn ) { continue; // PDO-only; PostgREST/Supabase is manual. } // Atomic ordering: backfill the column first, then re-apply // the collapsed function so there is never a window where // the no-fallback function queries a wrong-language column. $schema = new Gregius_Data_Schema_Manager(); $schema->rebuild_search_vector( $conn, $connection_name, $language ); $search = new Gregius_Data_Search_Schema(); $search->create_search_function( $conn, $connection_name ); } catch ( Exception $e ) { $logger = new Gregius_Data_Logger(); $logger->log( sprintf( 'Search vector language migration failed for connection "%s": %s', $connection_name, $e->getMessage() ), 'warning', 'search', $connection_name ); } } update_option( 'gregius_data_search_vector_language_migrated', true ); } /** * Re-apply the search function to install the `precomputed_query_vector` * overload of `search_rag_orchestrate` on existing installs. * * Runs once, guarded by a site option. PostgreSQL (PDO) only; Supabase * is manual (re-apply postgrest-schema.sql). * * @since 1.0.0 */ public static function migrate_search_rag_precomputed_query_vector() { if ( get_option( 'gregius_data_search_rag_precomputed_migrated' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_search_rag_precomputed_query_vector_for_current_site(); restore_current_blog(); } return; } self::migrate_search_rag_precomputed_query_vector_for_current_site(); } /** * Re-apply the search function for the current site. * * @return void */ private static function migrate_search_rag_precomputed_query_vector_for_current_site() { $settings_manager = new Gregius_Data_Settings_Manager(); $connections = method_exists( $settings_manager, 'get_all_connections' ) ? $settings_manager->get_all_connections() : array(); $db = new Gregius_Data_DB(); foreach ( array_keys( (array) $connections ) as $connection_name ) { try { $conn = $db->get_connection( $connection_name ); if ( ! $conn ) { continue; // PDO-only; PostgREST/Supabase is manual. } if ( class_exists( 'Gregius_Data_Search_Schema' ) ) { $search_schema = new Gregius_Data_Search_Schema(); $search_schema->create_search_function( $conn, $connection_name ); } } catch ( Exception $e ) { $logger = new Gregius_Data_Logger(); $logger->log( sprintf( 'Search function re-apply (precomputed query vector) failed for connection "%s": %s', $connection_name, $e->getMessage() ), 'warning', 'search', $connection_name ); } } update_option( 'gregius_data_search_rag_precomputed_migrated', true ); } /** * Re-apply the search functions to install best-match (OR) full-text * retrieval, replacing the strict-AND `plainto_tsquery` lexical leg. * * Runs once, guarded by a site option. PostgreSQL (PDO) only; Supabase * is manual (re-apply postgrest-schema.sql). * * @since 1.0.0 */ public static function migrate_search_bestmatch_fts() { if ( get_option( 'gregius_data_search_bestmatch_fts_migrated' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_search_bestmatch_fts_for_current_site(); restore_current_blog(); } return; } self::migrate_search_bestmatch_fts_for_current_site(); } /** * Re-apply the search functions for the current site. * * @since 1.0.0 */ private static function migrate_search_bestmatch_fts_for_current_site() { $settings_manager = new Gregius_Data_Settings_Manager(); $connections = method_exists( $settings_manager, 'get_all_connections' ) ? $settings_manager->get_all_connections() : array(); $db = new Gregius_Data_DB(); foreach ( array_keys( (array) $connections ) as $connection_name ) { try { $conn = $db->get_connection( $connection_name ); if ( ! $conn ) { continue; // PDO-only; PostgREST/Supabase is manual. } if ( class_exists( 'Gregius_Data_Search_Schema' ) ) { $search_schema = new Gregius_Data_Search_Schema(); $search_schema->create_search_function( $conn, $connection_name ); } } catch ( Exception $e ) { $logger = new Gregius_Data_Logger(); $logger->log( sprintf( 'Search function re-apply (best-match FTS) failed for connection "%s": %s', $connection_name, $e->getMessage() ), 'warning', 'search', $connection_name ); } } update_option( 'gregius_data_search_bestmatch_fts_migrated', true ); } /** * Remove the obsolete `typo_tolerance` and `similarity_threshold` search * settings on existing installs. * * These settings are no longer read: title trigram is now automatic for * the algorithmic (hashingtf) embedding, and the trigram similarity * threshold was removed. Runs once, guarded by a site option. * * @since 1.0.0 */ public static function migrate_remove_typo_tolerance_settings() { if ( get_option( 'gregius_data_typo_tolerance_settings_removed' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_remove_typo_tolerance_settings_for_current_site(); restore_current_blog(); } return; } self::migrate_remove_typo_tolerance_settings_for_current_site(); } /** * Delete the obsolete search settings for the current site. * * @since 1.0.0 */ private static function migrate_remove_typo_tolerance_settings_for_current_site() { $settings = new Gregius_Data_Settings_Manager(); $settings->delete_with_category( 'search', GREGIUS_DATA_SEARCH_SETTINGS_CONNECTION, 'typo_tolerance' ); $settings->delete_with_category( 'search', GREGIUS_DATA_SEARCH_SETTINGS_CONNECTION, 'similarity_threshold' ); update_option( 'gregius_data_typo_tolerance_settings_removed', true ); } /** * Remove the obsolete `observability_enabled` search setting on existing installs. * * The "Enhance Observability" expensive-probe toggle was removed; its setting * is no longer read. Runs once, guarded by a site option. * * @since 1.0.0 */ public static function migrate_remove_observability_setting() { if ( get_option( 'gregius_data_observability_setting_removed' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_remove_observability_setting_for_current_site(); restore_current_blog(); } return; } self::migrate_remove_observability_setting_for_current_site(); } /** * Delete the obsolete observability setting for the current site. * * @since 1.0.0 */ private static function migrate_remove_observability_setting_for_current_site() { $settings = new Gregius_Data_Settings_Manager(); $settings->delete_with_category( 'search', GREGIUS_DATA_SEARCH_SETTINGS_CONNECTION, 'observability_enabled' ); update_option( 'gregius_data_observability_setting_removed', true ); } /** * Flip the real-time sync default from false to true on existing installs. * * The real-time sync toggle was previously a no-op (post-save sync always ran * regardless of the stored value), and its stored default was false. Flipping * the default to true makes the setting reflect actual behavior before the * toggle is wired into the post-save path. Runs once, guarded by a site option. * * @since 1.0.1 */ public static function migrate_realtime_sync_default() { if ( get_option( 'gregius_data_real_time_sync_migrated' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_realtime_sync_default_for_current_site(); restore_current_blog(); } return; } self::migrate_realtime_sync_default_for_current_site(); } /** * Flip the real-time sync default for the current site. * * @since 1.0.1 */ private static function migrate_realtime_sync_default_for_current_site() { update_option( 'gregius_data_sync_real_time_sync', true ); update_option( 'gregius_data_real_time_sync_migrated', true ); } /** * Clear stale sync validation results cached before the 1.0.1 parity fix. * * Pre-1.0.1 releases could store a false "100% critical drift" result for * Supabase connections. Clear them on update so the cached results can never * be served stale (the next validation run repopulates them). Runs once, * guarded by a site option. * * @since 1.0.1 */ public static function migrate_clear_stale_validation_results() { if ( get_option( 'gregius_data_validation_results_cleared' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::migrate_clear_stale_validation_results_for_current_site(); restore_current_blog(); } return; } self::migrate_clear_stale_validation_results_for_current_site(); } /** * Clear stale sync validation results for the current site. * * @since 1.0.1 */ private static function migrate_clear_stale_validation_results_for_current_site() { global $wpdb; $like = $wpdb->esc_like( 'gregius_data_validation_results_' ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time migration; deletes cached validation results by prefix. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb, value prepared. $like ) ); update_option( 'gregius_data_validation_results_cleared', true ); } /** * Check plugin version and run upgrades if needed * Called on admin_init hook to catch updates while plugin is active */ public static function check_version() { $installed_version = get_option( self::VERSION_OPTION, '0.0.0' ); if ( version_compare( $installed_version, GREGIUS_DATA_VERSION, '<' ) ) { self::run_upgrades(); if ( function_exists( 'gregius_data_migrate_search_settings_scope' ) ) { gregius_data_migrate_search_settings_scope(); } self::ensure_prompt_infrastructure(); self::ensure_prompt_type_terms(); self::seed_default_prompts(); self::migrate_prompt_types(); self::migrate_factory_prompt_to_placeholders(); } self::migrate_hashingtf(); self::migrate_search_vector_language(); self::migrate_search_rag_precomputed_query_vector(); self::migrate_search_bestmatch_fts(); self::migrate_remove_typo_tolerance_settings(); self::migrate_remove_observability_setting(); self::migrate_realtime_sync_default(); self::migrate_clear_stale_validation_results(); // Seed factory prompts idempotently (each guarded by its own option) so // a new prompt type (e.g. evaluation) lands on existing installs without // a version bump. self::seed_default_prompts(); } /** * Run all necessary upgrade routines * Upgrades run in sequence based on version comparison */ private static function run_upgrades() { $installed_version = get_option( self::VERSION_OPTION, '0.0.0' ); // Update version number only after successful upgrades. update_option( self::VERSION_OPTION, GREGIUS_DATA_VERSION ); // Show admin notice for successful upgrade. if ( version_compare( $installed_version, GREGIUS_DATA_VERSION, '<' ) ) { add_action( 'admin_notices', function () use ( $installed_version ) { $class = 'notice notice-success is-dismissible'; $message = sprintf( /* translators: 1: old version, 2: new version */ __( 'Gregius Data updated from version %1$s to %2$s successfully.', 'gregius-data' ), $installed_version, GREGIUS_DATA_VERSION ); printf( '

%2$s

', esc_attr( $class ), esc_html( $message ) ); } ); } } /** * Initialize default sync settings for all sites. * * Starts with empty defaults to force user selection. On multisite, network * activation fires the activation hook once in the primary site context, * so each site is initialized explicitly here. * * @since 1.0.0 */ private static function init_default_sync_settings() { if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::init_default_sync_settings_for_current_site(); restore_current_blog(); } return; } self::init_default_sync_settings_for_current_site(); } /** * Initialize default sync settings for the current site. * * @since 1.0.0 */ private static function init_default_sync_settings_for_current_site() { // Empty defaults - user must explicitly choose what to sync. $defaults = array( 'enabled_post_types' => array(), 'enabled_statuses' => array(), 'real_time_sync' => true, 'sync_meta' => true, 'sync_terms' => true, ); // Store defaults in wp_options for global access. foreach ( $defaults as $key => $value ) { $option_name = 'gregius_data_sync_' . $key; if ( false === get_option( $option_name ) ) { update_option( $option_name, $value ); } } } /** * Schedule log retention event for each site context. * * @since 1.0.0 */ private static function schedule_log_retention_events() { if ( ! class_exists( 'Gregius_Data_Log_Retention' ) ) { return; } if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); Gregius_Data_Log_Retention::schedule_for_current_site(); restore_current_blog(); } return; } Gregius_Data_Log_Retention::schedule_for_current_site(); } /** * Schedule monitoring cron events for each site context. * * @since 1.0.0 */ private static function schedule_monitoring_events() { if ( is_multisite() ) { $sites = get_sites( array( 'number' => 0 ) ); foreach ( $sites as $site ) { switch_to_blog( (int) $site->blog_id ); self::schedule_monitoring_events_for_current_site(); restore_current_blog(); } return; } self::schedule_monitoring_events_for_current_site(); } /** * Schedule monitoring cron events for the current site. * * @since 1.0.0 */ private static function schedule_monitoring_events_for_current_site() { if ( ! wp_next_scheduled( 'gregius_data_check_connection_health' ) ) { wp_schedule_event( time(), 'gregius_data_every_five_minutes', 'gregius_data_check_connection_health' ); } if ( ! wp_next_scheduled( 'gregius_data_daily_validation' ) ) { wp_schedule_event( strtotime( 'tomorrow 3:00 AM' ), 'daily', 'gregius_data_daily_validation' ); } } } }