| @@ -17,9 +17,110 @@ | ||
| 17 | 17 | { |
| 18 | 18 | /** @var self|null */ |
| 19 | 19 | private static $instance = null; |
| 20 | 20 | |
| 21 | + const CACHE_KEY = 'metasync_admin_bar_status'; | |
| 22 | + | |
| 21 | 23 | /** |
| 24 | + * Menu item keys that retain a WordPress admin sidebar row in short mode. | |
| 25 | + * | |
| 26 | + * Everything else is registered with an empty parent slug | |
| 27 | + * (add_submenu_page('', ...)) so the page stays reachable at its | |
| 28 | + * existing URL but does not claim a sidebar row. The in-page sidebar | |
| 29 | + * (render_sidenav) continues to show every item. | |
| 30 | + */ | |
| 31 | + const WP_MENU_KEEP_LIST = [ | |
| 32 | + 'dashboard', // Dashboard | |
| 33 | + 'seo_controls', // Indexation Control | |
| 34 | + 'redirections', // Redirections | |
| 35 | + 'monitor_404', // 404 Monitor | |
| 36 | + 'xml_sitemap', // XML Sitemap | |
| 37 | + 'robots_txt', // Robots.txt | |
| 38 | + 'schema_markup', // Schema Markup | |
| 39 | + 'seo_health', // SEO Health | |
| 40 | + 'general', // Settings | |
| 41 | + ]; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * Whether the Settings page (general) is reachable for the current user. | |
| 45 | + * | |
| 46 | + * Settings is the floor — the top-level "Search Atlas" menu item is | |
| 47 | + * registered with the Settings callback, so it must always point at a | |
| 48 | + * page the user is permitted to see. When Settings is hidden by access | |
| 49 | + * control, the top-level callback falls back to the first reachable | |
| 50 | + * keep-list page (see resolve_top_level_callback()). | |
| 51 | + * | |
| 52 | + * @return bool | |
| 53 | + */ | |
| 54 | + public static function is_settings_reachable() | |
| 55 | + { | |
| 56 | + return Metasync_Access_Control::user_can_access('hide_settings'); | |
| 57 | + } | |
| 58 | + | |
| 59 | + /** | |
| 60 | + * Resolve the callback the top-level menu item should use. | |
| 61 | + * | |
| 62 | + * The top-level "Search Atlas" menu item is the floor — the plugin must | |
| 63 | + * always present at least one working entry point. When Settings is | |
| 64 | + * reachable it is the floor (its callback also renders the Settings | |
| 65 | + * page). When Settings is hidden, fall back to the first reachable | |
| 66 | + * keep-list page callback so the parent never leads to a permission | |
| 67 | + * error or blank page. | |
| 68 | + * | |
| 69 | + * @param Metasync_Admin $admin The admin instance whose callbacks are used. | |
| 70 | + * @return array WordPress-style callback array ([$admin, 'method']). | |
| 71 | + */ | |
| 72 | + public static function resolve_top_level_callback($admin) | |
| 73 | + { | |
| 74 | + if (self::is_settings_reachable()) { | |
| 75 | + return array($admin, 'create_admin_settings_page'); | |
| 76 | + } | |
| 77 | + | |
| 78 | + // Fall back to the first reachable keep-list item callback. The | |
| 79 | + // keep-list is ordered by priority (Dashboard, Indexation Control, | |
| 80 | + // Redirections, 404 Monitor, XML Sitemap, Robots.txt, SEO Health, | |
| 81 | + // Settings). We reuse get_available_menu_items() so access-control | |
| 82 | + // and connection-state gates are respected identically to the | |
| 83 | + // sidebar. | |
| 84 | + $available = self::instance()->get_available_menu_items(); | |
| 85 | + foreach (self::WP_MENU_KEEP_LIST as $key) { | |
| 86 | + if (isset($available[$key])) { | |
| 87 | + return array($admin, $available[$key]['callback']); | |
| 88 | + } | |
| 89 | + } | |
| 90 | + | |
| 91 | + // No keep-list item is reachable — keep Settings as the last resort | |
| 92 | + // so the menu never leads to a blank page. Settings itself handles | |
| 93 | + // its own access-control rendering. | |
| 94 | + return array($admin, 'create_admin_settings_page'); | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * The ordered keep-list of menu item keys that always retain a | |
| 99 | + * WordPress admin sidebar row (in short mode). | |
| 100 | + * | |
| 101 | + * @return string[] | |
| 102 | + */ | |
| 103 | + public static function get_wp_menu_keep_list() | |
| 104 | + { | |
| 105 | + return self::WP_MENU_KEEP_LIST; | |
| 106 | + } | |
| 107 | + | |
| 108 | + /** | |
| 109 | + * Whether a given menu item key retains a WordPress admin sidebar row. | |
| 110 | + * | |
| 111 | + * Only keep-list items retain a WordPress sidebar row. All other pages | |
| 112 | + * remain registered and available from the in-page navigation. | |
| 113 | + * | |
| 114 | + * @param string $item_key Menu item key from get_available_menu_items(). | |
| 115 | + * @return bool | |
| 116 | + */ | |
| 117 | + public static function item_keeps_wp_sidebar_row($item_key) | |
| 118 | + { | |
| 119 | + return in_array($item_key, self::WP_MENU_KEEP_LIST, true); | |
| 120 | + } | |
| 121 | + | |
| 122 | + /** | |
| 22 | 123 | * Get singleton instance. |
| 23 | 124 | * |
| 24 | 125 | * @return self |
| 25 | 126 | */ |
| @@ -32,8 +133,24 @@ | ||
| 32 | 133 | } |
| 33 | 134 | |
| 34 | 135 | private function __construct() {} |
| 35 | 136 | |
| 137 | + /** | |
| 138 | + * Whether the Dashboard is hidden by the "Hide Dashboard" general setting | |
| 139 | + * (hide_dashboard_framework). | |
| 140 | + * | |
| 141 | + * When enabled this removes the Dashboard from the WordPress admin submenu | |
| 142 | + * and from the in-plugin SEO feature navigation, matching the setting's | |
| 143 | + * description "Hide the main dashboard from the WordPress admin menu". | |
| 144 | + * | |
| 145 | + * @return bool | |
| 146 | + */ | |
| 147 | + public static function is_dashboard_hidden_by_framework() | |
| 148 | + { | |
| 149 | + $general_options = Metasync::get_option('general') ?? []; | |
| 150 | + return filter_var($general_options['hide_dashboard_framework'] ?? false, FILTER_VALIDATE_BOOLEAN); | |
| 151 | + } | |
| 152 | + | |
| 36 | 153 | // ------------------------------------------------------------------ |
| 37 | 154 | // Logo resolution helper |
| 38 | 155 | // ------------------------------------------------------------------ |
| 39 | 156 | |
| @@ -39,9 +156,9 @@ | ||
| 39 | 156 | |
| 40 | 157 | /** |
| 41 | 158 | * Resolve which logo(s) to display based on whitelabel settings. |
| 42 | 159 | * |
| 43 | - * @return array{show_logo: bool, use_dual: bool, url: string, light_url: string, dark_url: string} | |
| 160 | + * @return array{show_logo: bool, use_dual: bool, url: string, light_url: string, dark_url: string, is_default: bool} | |
| 44 | 161 | */ |
| 45 | 162 | private function resolve_logo_data() |
| 46 | 163 | { |
| 47 | 164 | $wl_settings = Metasync::get_whitelabel_settings(); |
| @@ -53,13 +170,14 @@ | ||
| 53 | 170 | $has_dark = !empty($dark_raw) && filter_var($dark_raw, FILTER_VALIDATE_URL); |
| 54 | 171 | $use_dual = ($has_light && $has_dark && $light_raw !== $dark_raw); |
| 55 | 172 | |
| 56 | 173 | $data = [ |
| 57 | - 'show_logo' => false, | |
| 58 | - 'use_dual' => false, | |
| 59 | - 'url' => '', | |
| 60 | - 'light_url' => '', | |
| 61 | - 'dark_url' => '', | |
| 174 | + 'show_logo' => false, | |
| 175 | + 'use_dual' => false, | |
| 176 | + 'url' => '', | |
| 177 | + 'light_url' => '', | |
| 178 | + 'dark_url' => '', | |
| 179 | + 'is_default' => false, | |
| 62 | 180 | ]; |
| 63 | 181 | |
| 64 | 182 | if ($use_dual) { |
| 65 | 183 | $data['show_logo'] = true; |
| @@ -71,10 +189,11 @@ | ||
| 71 | 189 | if ($single) { |
| 72 | 190 | $data['show_logo'] = true; |
| 73 | 191 | $data['url'] = $single; |
| 74 | 192 | } elseif (!$is_whitelabel) { |
| 75 | - $data['show_logo'] = true; | |
| 76 | - $data['url'] = Metasync::HOMEPAGE_DOMAIN . '/wp-content/uploads/2023/12/white.svg'; | |
| 193 | + $data['show_logo'] = true; | |
| 194 | + $data['url'] = plugin_dir_url(dirname(__FILE__)) . 'assets/images/searchatlas-logo.svg'; | |
| 195 | + $data['is_default'] = true; | |
| 77 | 196 | } |
| 78 | 197 | } |
| 79 | 198 | |
| 80 | 199 | return $data; |
| @@ -103,10 +222,11 @@ | ||
| 103 | 222 | $logo = $this->resolve_logo_data(); |
| 104 | 223 | |
| 105 | 224 | $current_theme = get_option('metasync_theme', 'dark'); |
| 106 | 225 | $general_settings = Metasync::get_option('general'); |
| 107 | - $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : ''; | |
| 108 | - $is_integrated = !empty($searchatlas_api_key); | |
| 226 | + // derive the badge from confirmed heartbeat health, not from | |
| 227 | + // mere API-key presence (which stays set after Search Atlas revokes it). | |
| 228 | + $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general_settings); | |
| 109 | 229 | ?> |
| 110 | 230 | <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>"> |
| 111 | 231 | <div class="metasync-header-left"> |
| 112 | 232 | <?php if ($logo['show_logo'] && $logo['use_dual']): ?> |
| @@ -115,16 +235,16 @@ | ||
| 115 | 235 | <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" /> |
| 116 | 236 | </div> |
| 117 | 237 | <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?> |
| 118 | 238 | <div class="metasync-logo-container"> |
| 119 | - <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo" /> | |
| 239 | + <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" /> | |
| 120 | 240 | </div> |
| 121 | 241 | <?php endif; ?> |
| 122 | 242 | </div> |
| 123 | 243 | <div class="metasync-header-right"> |
| 124 | - <div class="metasync-status <?php echo $is_integrated ? 'connected' : 'disconnected'; ?>"> | |
| 244 | + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>"> | |
| 125 | 245 | <span class="status-dot"></span> |
| 126 | - <span class="status-text"><?php echo $is_integrated ? 'Connected' : 'Not Connected'; ?></span> | |
| 246 | + <span class="status-text"><?php echo esc_html($badge['text']); ?></span> | |
| 127 | 247 | </div> |
| 128 | 248 | <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme"> |
| 129 | 249 | <span class="theme-icon-light">☀</span> |
| 130 | 250 | <span class="theme-icon-dark">☾</span> |
| @@ -146,9 +266,8 @@ | ||
| 146 | 266 | $menu_items = []; |
| 147 | 267 | $menu_icons = [ |
| 148 | 268 | 'dashboard' => 'dashboard', |
| 149 | 269 | 'seo_controls' => 'search', |
| 150 | - 'optimal_settings' => 'superhero-alt', | |
| 151 | 270 | 'instant_index' => 'performance', |
| 152 | 271 | 'google_console' => 'chart-area', |
| 153 | 272 | 'compatibility' => 'admin-tools', |
| 154 | 273 | 'sync_log' => 'list-view', |
| @@ -159,9 +278,9 @@ | ||
| 159 | 278 | 'report_issue' => 'sos', |
| 160 | 279 | 'general' => 'admin-settings', |
| 161 | 280 | ]; |
| 162 | 281 | |
| 163 | - if (empty($whitelabel_settings['hide_dashboard'])) { | |
| 282 | + if (empty($whitelabel_settings['hide_dashboard']) && !self::is_dashboard_hidden_by_framework()) { | |
| 164 | 283 | $menu_items['dashboard'] = ['title' => 'Dashboard', 'slug_suffix' => '-dashboard']; |
| 165 | 284 | } |
| 166 | 285 | |
| 167 | 286 | if (empty($whitelabel_settings['hide_indexation_control'])) { |
| @@ -166,17 +285,9 @@ | ||
| 166 | 285 | |
| 167 | 286 | if (empty($whitelabel_settings['hide_indexation_control'])) { |
| 168 | 287 | $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls']; |
| 169 | 288 | } |
| 170 | - | |
| 171 | - if ($general_options['enable_optimal_settings'] ?? false) { | |
| 172 | - $menu_items['optimal_settings'] = ['title' => 'Optimal Settings', 'slug_suffix' => '-optimal-settings']; | |
| 173 | - } | |
| 174 | - | |
| 175 | - if ($general_options['enable_googleinstantindex'] ?? false) { | |
| 176 | - $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index']; | |
| 177 | - } | |
| 178 | - | |
| 289 | + | |
| 179 | 290 | if ($general_options['enable_google_console'] ?? false) { |
| 180 | 291 | $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console']; |
| 181 | 292 | } |
| 182 | 293 | |
| @@ -202,9 +313,9 @@ | ||
| 202 | 313 | <div class="metasync-nav-tabs"> |
| 203 | 314 | <div class="metasync-nav-left"> |
| 204 | 315 | <?php foreach ($menu_items as $key => $menu_item): |
| 205 | 316 | $is_active = ($current_page === $key); |
| 206 | - $icon = $menu_icons[$key] ?? 'admin-generic'; | |
| 317 | + $icon = $menu_icons[$key]; | |
| 207 | 318 | $page_url = '?page=' . $page_slug . $menu_item['slug_suffix']; |
| 208 | 319 | ?> |
| 209 | 320 | <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>"> |
| 210 | 321 | <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span> |
| @@ -369,10 +480,10 @@ | ||
| 369 | 480 | $menu_items = []; |
| 370 | 481 | |
| 371 | 482 | // === SEO FEATURES GROUP === |
| 372 | 483 | |
| 373 | - // Dashboard (check access control) | |
| 374 | - if (Metasync_Access_Control::user_can_access('hide_dashboard')) { | |
| 484 | + // Dashboard (check access control + "Hide Dashboard" general setting) | |
| 485 | + if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) { | |
| 375 | 486 | $menu_items['dashboard'] = [ |
| 376 | 487 | 'title' => 'Dashboard', |
| 377 | 488 | 'slug_suffix' => '-dashboard', |
| 378 | 489 | 'callback' => 'create_admin_dashboard_iframe', |
| @@ -391,20 +502,8 @@ | ||
| 391 | 502 | 'group' => 'seo' |
| 392 | 503 | ]; |
| 393 | 504 | } |
| 394 | 505 | |
| 395 | - // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control) | |
| 396 | - $seo_controls = Metasync::get_option('seo_controls'); | |
| 397 | - if ($seo_controls['enable_googleinstantindex'] ?? false) { | |
| 398 | - $menu_items['instant_index'] = [ | |
| 399 | - 'title' => 'Instant Indexing', | |
| 400 | - 'slug_suffix' => '-instant-index', | |
| 401 | - 'callback' => 'create_admin_google_instant_index_page', | |
| 402 | - 'internal_nav' => 'Instant Indexing', | |
| 403 | - 'group' => 'seo' | |
| 404 | - ]; | |
| 405 | - } | |
| 406 | - | |
| 407 | 506 | if ($general_options['enable_google_console'] ?? false) { |
| 408 | 507 | $menu_items['google_console'] = [ |
| 409 | 508 | 'title' => 'Google Console', |
| 410 | 509 | 'slug_suffix' => '-google-console', |
| @@ -552,8 +651,17 @@ | ||
| 552 | 651 | 'internal_nav' => 'Code Minification', |
| 553 | 652 | 'group' => 'plugin' |
| 554 | 653 | ]; |
| 555 | 654 | |
| 655 | + // Media Optimization | |
| 656 | + $menu_items['media_optimization'] = [ | |
| 657 | + 'title' => 'Media Optimization', | |
| 658 | + 'slug_suffix' => '-media-optimization', | |
| 659 | + 'callback' => 'create_admin_media_optimization_page', | |
| 660 | + 'internal_nav' => 'Media Optimization', | |
| 661 | + 'group' => 'plugin' | |
| 662 | + ]; | |
| 663 | + | |
| 556 | 664 | // Custom HTML Pages (check access control) |
| 557 | 665 | if (Metasync_Access_Control::user_can_access('hide_custom_pages')) { |
| 558 | 666 | $menu_items['custom_pages'] = [ |
| 559 | 667 | 'title' => 'Custom Pages', |
| @@ -563,18 +671,8 @@ | ||
| 563 | 671 | 'group' => 'plugin' |
| 564 | 672 | ]; |
| 565 | 673 | } |
| 566 | 674 | |
| 567 | - if ($general_options['enable_optimal_settings'] ?? false) { | |
| 568 | - $menu_items['optimal_settings'] = [ | |
| 569 | - 'title' => 'Optimal Settings', | |
| 570 | - 'slug_suffix' => '-optimal-settings', | |
| 571 | - 'callback' => 'create_admin_optimal_settings_page', | |
| 572 | - 'internal_nav' => 'Optimal Settings', | |
| 573 | - 'group' => 'plugin' | |
| 574 | - ]; | |
| 575 | - } | |
| 576 | - | |
| 577 | 675 | // Compatibility page (check access control) |
| 578 | 676 | if (Metasync_Access_Control::user_can_access('hide_compatibility')) { |
| 579 | 677 | $menu_items['compatibility'] = [ |
| 580 | 678 | 'title' => 'Compatibility', |
| @@ -633,9 +731,11 @@ | ||
| 633 | 731 | $data = Metasync::get_option('general'); |
| 634 | 732 | $plugin_name = Metasync::get_effective_plugin_name(); |
| 635 | 733 | $menu_name = $plugin_name; |
| 636 | 734 | $menu_title = $plugin_name; |
| 637 | - $menu_slug = !isset($data['white_label_plugin_menu_slug']) || $data['white_label_plugin_menu_slug'] == "" ? Metasync_Admin::$page_slug : $data['white_label_plugin_menu_slug']; | |
| 735 | + // Sanitize so a URL-shaped legacy value still yields a valid WP menu slug | |
| 736 | + $menu_slug = !isset($data['white_label_plugin_menu_slug']) || $data['white_label_plugin_menu_slug'] == "" ? Metasync_Admin::$page_slug : sanitize_title($data['white_label_plugin_menu_slug']); | |
| 737 | + $menu_slug = $menu_slug === '' ? Metasync_Admin::$page_slug : $menu_slug; | |
| 638 | 738 | $menu_icon = !isset($data['white_label_plugin_menu_icon']) || $data['white_label_plugin_menu_icon'] == "" ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzQiIHZpZXdCb3g9IjAgMCAzMiAzNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzE4NzlfMTcyNzcpIj4KPHBhdGggZD0iTTI5LjAyMTUgMi4xNjc2NkwzMC4wMTAxIDIuMTIyNDFMMjkuMTYyNyA4LjcyNzA1TDI5LjExNTcgOC44MTc0M0w1LjI5NTA0IDMzLjI0NTJMNC43MzAxMiAzMy4yOTA1TDQuMDcxMDQgMzMuOTY5MUgxLjk1MjYyTDIuMjM1MDggMzIuMjk1M0wyLjA5Mzg0IDMyLjM0MDZMMi4xODggMzEuNjYySDEuNjIzMDhDMS41NzYgMzEuNjYyIDEuNDgxODUgMzEuNjYyIDEuNDgxODUgMzEuNjE2N0MxLjQzNDc4IDMxLjU3MTYgMS4zODc3IDMxLjQ4MSAxLjM4NzcgMzEuNDM1OEwyLjUxNzU0IDI1LjEwMjdDMi41MTc1NCAyNS4wNTc1IDIuNTY0NiAyNS4wMTIyIDIuNTY0NiAyNS4wMTIyTDI2LjE5NjkgMC42Mjk2MDdDMjYuMjQ0IDAuNTg0MzY2IDI2LjI5MTEgMC41MzkxMjUgMjYuMzM4MSAwLjUzOTEyNUgyOC4zNjI1QzI4LjQwOTUgMC40OTM4ODQgMjguNDU2NiAwLjUzOTEyNSAyOC41MDM3IDAuNTg0MzY2QzI4LjU1MDcgMC42Mjk2MDcgMjguNTk3OSAwLjcyMDA3MSAyOC41OTc5IDAuNzY1MzExTDI4LjUwMzcgMS4zNTMzOUgyOS4xMTU3TDI5LjAyMTUgMi4xNjc2NlpNMS45MDU1NCAzMS4yMDk2SDIuMjgyMTZMMy4yMjM2OCAyNS43ODEySDMuMjcwNzZMNi44MDE1IDIyLjI1MjhMNi44NDg1MSAyMi4yOTgxTDIwLjIxODMgOC40NTU1N0wyNi45MDMxIDEuMzUzMzlIMjguMDMyOUwyOC4wNzk5IDAuOTkxNDk5SDI2LjQ3OTNMMi45ODgzIDI1LjIzODRMMS45MDU1NCAzMS4yMDk2Wk0yOC42OTE5IDguNTQ1OTVMMjkuNDQ1MiAyLjYyMDAxSDI4Ljk3NDVMMjguMzE1MyA3Ljc3Njk2TDI4LjI2ODMgNy44MjIyNEwyOC4xMjcxIDcuOTU3ODlMMjcuOTM4NyA5LjMxNTExTDI4LjY5MTkgOC41NDU5NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zMS41NTQ0IDE4LjE5MzdDMzEuNzM0OSAxOC42NjYyIDMxLjgwMjggMTkuMjA2NCAzMS42ODk4IDE5Ljc2OUwyOS42NTgyIDMyLjEwMTNIMjkuMDkzOUwyOS4wMjYyIDMyLjQzODhIMjUuOTMzNkwyNi4wNjkxIDMxLjYwNjJIMjYuMDAxM0wyNi4wNjkxIDMxLjI2ODZIMjUuNzk4MUMyNS43NTMgMzEuMjY4NiAyNS43MzA1IDMxLjI2ODYgMjUuNzA3OSAzMS4yNDYxQzI1LjY4NTIgMzEuMjIzNyAyNS42ODUyIDMxLjE3ODYgMjUuNjg1MiAzMS4xNTZMMjYuMzYyNCAyNy4zOThIMTguNDE2N0wxNy40MjM0IDMyLjA3ODdIMTYuODM2NUwxNi43Njg3IDMyLjQxNjNIMTMuNjk4OEwxMy45MDE5IDMxLjU4MzZIMTMuODM0M0wxMy45MDE5IDMxLjI2ODZIMTMuNjMxQzEzLjYwODQgMzEuMjY4NiAxMy41NjMzIDMxLjI2ODYgMTMuNTQwOCAzMS4yNDYxQzEzLjU0MDggMzEuMjAxMSAxMy41MTgyIDMxLjE3ODYgMTMuNTQwOCAzMS4xMzM2TDE2LjM2MjQgMTguOTEzOEMxNi40OTc5IDE4LjM1MTIgMTYuNzQ2MiAxNy44MzM2IDE3LjEyOTkgMTcuMzM4NUMxNy40OTEyIDE2Ljg2NiAxNy45NDI2IDE2LjQ4MzUgMTguNDYxOCAxNi4yMTM0QzE4Ljk4MSAxNS45MjA3IDE5LjUwMDIgMTUuNzg1OCAyMC4wNDE5IDE1Ljc4NThIMjguNTk3MkMyOS4xMzkgMTUuNzg1OCAyOS42MTMxIDE1LjkyMDcgMzAuMDE5MyAxNi4yMTM0QzMwLjM1NzkgMTYuNDYwOSAzMC42Mjg5IDE2Ljc5ODUgMzAuODA5NSAxNy4xODFDMzEuMTI1NSAxNy40NTExIDMxLjM5NjMgMTcuNzg4NiAzMS41NTQ0IDE4LjE5MzdaTTI3LjQ5MTIgMjMuMzY5N0wyOC4wNTU1IDIwLjI2NDFDMjguMDMyOSAyMC4yNDE1IDI4LjAzMjkgMjAuMjE5MSAyOC4wMTA0IDIwLjIxOTFIMjcuODk3NUwyNy4zNzgzIDIzLjA5OTZDMjcuMzc4MyAyMy4xNDQ3IDI3LjMxMDYgMjMuMTg5NiAyNy4yNjU1IDIzLjE4OTZIMTkuMzQyMUwxOS4yOTcgMjMuMzY5N0gyNy40OTEyWk0xOS4wOTM5IDIzLjE4OTZIMTguODQ1NUwxOC44MjI5IDIzLjM2OTdIMTkuMDcxM0wxOS4wOTM5IDIzLjE4OTZaTTE5LjUwMDIgMjAuMjY0MUwxOC45MTMzIDIyLjk2NDZIMTkuMTYxNUwxOS43NDg0IDIwLjIxOTFIMTkuNTQ1M0MxOS41NDUzIDIwLjIxOTEgMTkuNTIyNyAyMC4yNDE1IDE5LjUwMDIgMjAuMjY0MVpNMjcuNjcxOCAyMC4yMTkxSDE5Ljk3NDJMMTkuMzg3MiAyMi45NjQ2SDI3LjE3NTFMMjcuNjcxOCAyMC4yMTkxWk0xMy43ODkgMzEuMDQzNkgxMy45NDdMMTUuMDk4NCAyNi4xMTUySDE1LjE2NkwxNi43MjM2IDE5LjI5NjRDMTYuODU5MSAxOC43NTYzIDE3LjEwNzMgMTguMjM4OCAxNy40Njg1IDE3Ljc2NjFDMTcuODI5NiAxNy4zMTYxIDE4LjI4MTIgMTYuOTMzNCAxOC43Nzc4IDE2LjY2MzNDMTkuMDI2MSAxNi41Mjg0IDE5LjI3NDUgMTYuNDM4NCAxOS41MjI3IDE2LjM0ODNMMTkuNTAwMiAxNi4zMDM0QzE5Ljc0ODQgMTYuMjEzNCAyMC4wMTkzIDE2LjE5MDggMjAuMjkwMyAxNi4xOTA4SDI4Ljg2ODJDMjkuMjI5NCAxNi4xOTA4IDI5LjU5MDUgMTYuMjU4MyAyOS44ODQgMTYuMzkzNEMyOS41MjI5IDE2LjEyMzMgMjkuMDkzOSAxNi4wMTA4IDI4LjU5NzIgMTYuMDEwOEgyMC4wNDE5QzE5LjU0NTMgMTYuMDEwOCAxOS4wNDg2IDE2LjE0NTkgMTguNTc0NyAxNi4zOTM0QzE4LjA3OCAxNi42NjMzIDE3LjY0OTEgMTcuMDIzNSAxNy4zMTA0IDE3LjQ5NkMxNi45NDkzIDE3Ljk0NjIgMTYuNzAxIDE4LjQ0MTIgMTYuNTg4MSAxOC45NTg5TDEzLjc4OSAzMS4wNDM2Wk0xNy4yMjAyIDMxLjg1MzdMMTguMTkxIDI3LjM5OEgxNy45NDI2TDE3LjAxNzEgMzEuNTgzNkgxNi45NDkzTDE2LjkwNDIgMzEuODUzN0gxNy4yMjAyWk0yNS45MzM2IDMxLjA0MzZIMjYuMTE0MkwyNi43Njg5IDI3LjM5OEgyNi41ODgzTDI1LjkzMzYgMzEuMDQzNlpNMzEuNDg2NyAxOS43NDY0QzMxLjU3NjkgMTkuMjA2NCAzMS41MDkzIDE4LjcxMTMgMzEuMzI4NyAxOC4yNjEyQzMxLjI4MzYgMTguMTQ4OCAzMS4yMTU4IDE4LjAzNjIgMzEuMTQ4MSAxNy45MjM2QzMxLjI4MzYgMTguMzUxMiAzMS4zMjg3IDE4LjgwMTQgMzEuMjM4MyAxOS4yOTY0TDMwLjA4NzIgMjYuMTE1MkwzMC4xNTQ4IDI2LjEzNzdMMjkuMjUxOSAzMS42MDYySDI5LjE4NDNMMjkuMTM5IDMxLjg3NjNIMjkuNDU1TDMxLjQ4NjcgMTkuNzQ2NFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNy42ODQ3IDIuNDI3MTdIMTcuNjYxOEMxNy44NjgyIDIuODk5MTIgMTcuOTE0IDMuNDM4NDkgMTcuODIyMiA0LjAwMDMzTDE3LjU5MjkgNS4zOTM3SDE3LjA0MjNMMTYuOTczNSA1LjczMDhIMTMuOTY4NEwxNC4xMjkxIDQuODk5MjhIMTQuMDM3M0wxNC4xMDYyIDQuNTg0NjRIMTMuODUzOEMxMy44MDggNC41ODQ2NCAxMy43ODUxIDQuNTYyMTcgMTMuNzYyIDQuNTM5NjlDMTMuNzM5MSA0LjUxNzIzIDEzLjczOTEgNC40OTQ3NSAxMy43MzkxIDQuNDQ5OEg1LjkxNjg2TDUuNTQ5ODcgNi4wOTAzOUgxMy41Nzg1QzE0LjEyOTEgNi4wOTAzOSAxNC42MzM3IDYuMjQ3NzEgMTUuMDQ2NyA2LjUzOTg3QzE1LjM2NzggNi43NDIxMyAxNS41OTcxIDcuMDU2NzcgMTUuODAzNiA3LjM3MTQxQzE1Ljg0OTUgNy40Mzg4OSAxNS44NzI0IDcuNDgzODIgMTUuOTE4NCA3LjU1MTEyQzE2LjIxNjYgNy43OTgzMiAxNi40Njg4IDguMTEyOTkgMTYuNjI5NSA4LjQ5NTE0QzE2LjgzNTkgOC45NjY5OCAxNi45MDQ4IDkuNTA2NDcgMTYuNzkwMSAxMC4wOTA3TDE2LjI4NTMgMTMuMTY5NkMxNi4xOTM1IDEzLjczMTUgMTUuOTQxMyAxNC4yNDg0IDE1LjU3NDMgMTQuNzQyOEMxNS4yMDcyIDE1LjIxNDcgMTQuNzQ4NCAxNS41OTY4IDE0LjIyMDggMTUuODg4OUMxNC4xNTIgMTUuOTExNSAxNC4wNjAyIDE1Ljk1NjQgMTMuOTkxNSAxNS45Nzg4QzEzLjg3NjcgMTYuMDY4OCAxMy43NjIgMTYuMTU4NyAxMy42MjQ1IDE2LjIyNkMxMy4wOTY4IDE2LjQ5NTcgMTIuNTY5MiAxNi42NTMxIDExLjk5NTcgMTYuNjUzMUgyLjYzNjYxQzIuMDg2MDcgMTYuNjUzMSAxLjU4MTQxIDE2LjQ5NTcgMS4xNjg1IDE2LjIyNkMwLjc1NTYxIDE1Ljk1NjQgMC40ODAzMzEgMTUuNTc0MyAwLjMxOTc1IDE1LjEyNDhDMC4xODIxMiAxNC43NjUyIDAuMTU5MTg3IDE0LjQwNTggMC4xODIxMTkgMTQuMDAxMkMwLjE4MjExOSAxMy45NTYxIDAuMTM2MjU0IDEzLjkzMzggMC4xMzYyNTQgMTMuODg4OEMtMC4wMjQzMjYxIDEzLjQxNjggLTAuMDQ3MjU4MSAxMi44Nzc1IDAuMDkwMzcyNSAxMi4zMTU2TDAuMzg4NTgzIDExLjAxMjJDMC4zODg1ODMgMTAuOTY3MyAwLjQ1NzM5OCAxMC45MjIzIDAuNTAzMjY0IDEwLjkyMjNIMy41NTQxN0MzLjU3NzExIDEwLjkyMjMgMy42MDAwNCAxMC45NDQ3IDMuNjIyOTkgMTAuOTY3M0MzLjY0NTkyIDEwLjk4OTYgMy42Njg4NyAxMS4wMzQ2IDMuNjQ1OTIgMTEuMDU3MUwzLjYwMDA0IDExLjMyNjlIMy44OTgyNUwzLjgwNjUgMTEuNzMxNEg0LjMxMTE2TDQuMjE5MzkgMTIuMjAzMkgxMi4yNzFDMTIuMjcxIDEyLjIwMzIgMTIuMjcxIDEyLjIwMzIgMTIuMjkzOSAxMi4xODA4QzEyLjI5MzkgMTIuMTU4MyAxMi4zMTY4IDEyLjE1ODMgMTIuMzE2OCAxMi4xNTgzTDEyLjU5MjEgMTAuNTYyN0gzLjk5MDAyQzMuNDM5NDggMTAuNTYyNyAyLjk1Nzc1IDEwLjQyNzggMi41Njc3OSAxMC4xMzU2QzIuMTU0ODggOS44NjU5IDEuODc5NjIgOS41MDY0NyAxLjcxOTA0IDkuMDM0NDZDMS42MDQzNCA4LjY5NzQxIDEuNTU4NDYgOC4zMzc4MSAxLjYwNDM0IDcuOTMzMjhDMS42MDQzNCA3Ljg4ODM1IDEuNTU4NDYgNy44NjU4IDEuNTU4NDYgNy44MjA4N0MxLjM5NzkgNy4zNDg4NiAxLjM3NDk3IDYuODA5NTYgMS41MTI2IDYuMjI1MjNMMi4yNDY2NSAzLjE0NjM0QzIuMzg0MjggMi41ODQ0OSAyLjYzNjYxIDIuMDY3NTggMy4wMDM2MyAxLjU3MzE2QzMuMzcwNjYgMS4xMDEyMiAzLjgyOTQ0IDAuNzE5MTUyIDQuMzU3MDQgMC40NDk0NzZDNC44ODQ2MyAwLjE1NzMxOSA1LjQxMjI0IDAgNS45NjI4MyAwSDE0LjcwMjZDMTUuMjMwMSAwIDE1LjcxMTggMC4xNTczMTkgMTYuMTI0OCAwLjQ0OTQ3NkMxNi40NDU5IDAuNjc0MjA2IDE2LjY3NTMgMC45NjYzOCAxNi44NTg4IDEuMzAzNDhDMTYuOTA0OCAxLjM0ODQzIDE2LjkyNzcgMS4zOTMzNyAxNi45NzM1IDEuNDYwOEMxNy4yNzE4IDEuNzMwNDggMTcuNTI0IDIuMDIyNjQgMTcuNjg0NyAyLjQyNzE3Wk0zLjY0NTkyIDEyLjU4NTRWMTIuNjA3OEgzLjg5ODI1TDMuOTIxMiAxMi40MjhIMy42Njg4N0wzLjY0NTkyIDEyLjU2MjhDMy42MjI5OSAxMi41NjI4IDMuNjIyOTkgMTIuNTg1NCAzLjY0NTkyIDEyLjU4NTRaTTQuOTk5MzMgNi41NjIzNUg1LjIwNTc4TDUuMjc0NjEgNi4zMTUxNEg0Ljk1MzQ1TDQuOTA3NTggNi40NDk5N0M0LjkwNzU4IDYuNDk0OTIgNC45MDc1OCA2LjUxNzQgNC45MzA1MSA2LjUzOTg3QzQuOTUzNDUgNi41NjIzNSA0Ljk3NjQgNi41NjIzNSA0Ljk5OTMzIDYuNTYyMzVaTTEuNzQxOTcgNi4yNzAxN0MxLjY1MDIzIDYuNjc0NyAxLjY1MDIzIDcuMDU2NzcgMS43MTkwNCA3LjM5Mzc5TDEuNzQxOTcgNy4yMzY1NUMxLjc2NDkyIDcuMDExODEgMS43ODc4NiA2LjgwOTU2IDEuODMzNzQgNi41ODQ4MUwyLjU0NDg0IDMuNTI4MzlDMi42ODI0OSAyLjk2NjU0IDIuOTM0ODIgMi40NDk2NSAzLjMwMTg1IDEuOTc3NjlDMy4zNDc3MSAxLjkzMjc0IDMuMzcwNjYgMS44ODc4IDMuMzkzNTkgMS44NDI4NUwzLjQ2MjQxIDEuOTEwMjhDMy44MDY1IDEuNDgzMjcgNC4xOTY0NiAxLjE0NjE2IDQuNjc4MTkgMC44OTg5NTNDNS4xODI4NCAwLjYyOTI2IDUuNjg3NSAwLjQ5NDQyMyA2LjIxNTA2IDAuNDk0NDIzSDE0Ljk3NzlDMTUuMTg0MyAwLjQ5NDQyMyAxNS4zOTA3IDAuNTE2OTA0IDE1LjU5NzEgMC41NjE4NUwxNS42MiAwLjQ5NDQyM0MxNS43MzQ5IDAuNTE2OTA0IDE1Ljg0OTUgMC41NjE4NSAxNS45NjQyIDAuNjA2Nzk2QzE1LjU5NzEgMC4zNTk1ODUgMTUuMTg0MyAwLjI0NzIxMSAxNC43MDI2IDAuMjQ3MjExSDUuOTYyODNDNS40NTgxMiAwLjI0NzIxMSA0Ljk1MzQ1IDAuMzU5NTg0IDQuNDcxNzQgMC42MjkyNkMzLjk2NzA3IDAuODk4OTUzIDMuNTU0MTcgMS4yNTg1NCAzLjE4NzE1IDEuNzA4MDFDMi44NDMwNSAyLjE3OTk1IDIuNTkwNzIgMi42NzQzOCAyLjQ3NjAzIDMuMTkxMjhMMS43NDE5NyA2LjI3MDE3Wk0xMi4yNzEgMTIuNDI4SDQuMTczNTNMNC4xMjc2NSAxMi42MDc4SDcuNzI5MVYxMi42NzUySDEyLjU2OTJDMTIuNTkyMSAxMi42NzUyIDEyLjYxNSAxMi42NTI3IDEyLjY2MSAxMi42MzAzQzEyLjY4MzkgMTIuNjA3OCAxMi43MDY4IDEyLjU2MjggMTIuNzA2OCAxMi41NDAzTDEyLjg0NDUgMTEuODIxMkwxMy4wNTEgMTAuNjc1QzEzLjA3MzkgMTAuNjMgMTMuMDUxIDEwLjYwNzcgMTMuMDI4MSAxMC41ODUxQzEzLjAwNSAxMC41NjI3IDEyLjk4MjEgMTAuNTYyNyAxMi45NTkyIDEwLjU2MjdIMTIuODQ0NUwxMi41MjMzIDEyLjIwMzJDMTIuNTIzMyAxMi4yNDgyIDEyLjUwMDQgMTIuMzE1NiAxMi40MzE3IDEyLjM2MDZDMTIuMzg1NyAxMi40MDU1IDEyLjMxNjggMTIuNDI4IDEyLjI3MSAxMi40MjhaTTQuMDM1OSAxMS45NTZIMy43NjA2MkwzLjcxNDc0IDEyLjIwMzJIMy45NjcwN0w0LjAzNTkgMTEuOTU2Wk0wLjI5NjgxNyAxMi4zNjA2QzAuMjA1MDY5IDEyLjc2NTEgMC4yMDUwNyAxMy4xNjk2IDAuMjczODg2IDEzLjUyOTJMMC4zMTk3NSAxMy4zMDQ0QzAuMzE5NzUgMTMuMTAyMSAwLjM0MjcgMTIuODk5OSAwLjQxMTUxNiAxMi42NzUyTDAuNzA5NzI3IDExLjMyNjlIMy4zNzA2NkwzLjM5MzU5IDExLjE0N0gwLjU5NTAyOUwwLjI5NjgxNyAxMi4zNjA2Wk0xNi41ODM1IDEwLjA0NThDMTYuNjc1MyA5LjUwNjQ3IDE2LjYwNjYgOS4wMTE5MSAxNi40MjMgOC41ODVDMTYuNDAwMSA4LjU0MDA3IDE2LjM3NzEgOC40OTUxNCAxNi4zNTQyIDguNDUwMjFDMTYuNDAwMSA4LjY1MjQ4IDE2LjQyMyA4Ljg1NDc0IDE2LjQyMyA5LjA3OTM5QzE2LjQyMyA5LjI1OTI3IDE2LjQyMyA5LjQzODk5IDE2LjM3NzEgOS42MTg3TDE1Ljg3MjQgMTIuNjk3NkMxNS44MjY2IDEyLjkyMjQgMTUuNzU3OCAxMy4xMjQ3IDE1LjY4ODkgMTMuMzI3TDE1LjY0MzEgMTMuNTk2N0MxNS41NTE0IDE0LjA2ODUgMTUuMzQ0OSAxNC41MTggMTUuMDQ2NyAxNC45NDUxQzE1LjE2MTQgMTQuODMyNyAxNS4yOTkgMTQuNzIwMyAxNS4zOTA3IDE0LjYwOEMxNS43MzQ5IDE0LjE1ODQgMTUuOTY0MiAxMy42NDE2IDE2LjA1NiAxMy4xMjQ3TDE2LjU4MzUgMTAuMDQ1OFpNMTMuNTc4NSA2LjMxNTE0SDUuNTAzOTlMNS40NTgxMiA2LjU2MjM1SDEwLjA5MTdWNi40OTQ5MkgxMy44NTM4QzE0LjI0MzcgNi40OTQ5MiAxNC41ODc5IDYuNTYyMzUgMTQuOTA5IDYuNzE5NjVDMTQuNTE5IDYuNDQ5OTcgMTQuMDgzMyA2LjMxNTE0IDEzLjU3ODUgNi4zMTUxNFpNNS4zMjA0NyA2LjA5MDM5TDUuNjg3NSA0LjQ0OThINS40NTgxMkM1LjQzNTE3IDQuNDQ5OCA1LjQxMjI0IDQuNDcyMjggNS4zODkyOSA0LjQ5NDc1QzUuMzQzNDIgNC41MTcyMyA1LjM0MzQyIDQuNTYyMTggNS4zMjA0NyA0LjU4NDY0TDQuOTk5MzMgNi4wOTAzOUg1LjMyMDQ3Wk0xNy41OTI5IDMuOTc3ODZDMTcuNjg0NyAzLjQzODQ5IDE3LjYzODcgMi45NDQwNyAxNy40NTUyIDIuNDk0NTlDMTcuNDU1MiAyLjQ3MjExIDE3LjQwOTQgMi40NDk2NSAxNy40MDk0IDIuNDA0NjhDMTcuNDc4MyAyLjc2NDI3IDE3LjUwMTEgMy4xNDYzNCAxNy40MzIzIDMuNTUwODVMMTcuMjAzIDQuODk5MjhIMTcuMTM0MUwxNy4wODgzIDUuMTY4OTdIMTcuNDA5NEwxNy41OTI5IDMuOTc3ODZaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzE4NzlfMTcyNzciPgo8cmVjdCB3aWR0aD0iMzEuNzQ0OSIgaGVpZ2h0PSIzNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K' : $data['white_label_plugin_menu_icon']; |
| 639 | 739 | |
| 640 | 740 | // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above |
| 641 | 741 | $menu_capability = 'read'; |
| @@ -645,15 +745,19 @@ | ||
| 645 | 745 | global $menu; |
| 646 | 746 | $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator'); |
| 647 | 747 | }, 5); |
| 648 | 748 | |
| 649 | - // Main menu page at position 100.1 — bottom of the menu after all standard items | |
| 749 | + // Main menu page at position 100.1 — bottom of the menu after all standard items. | |
| 750 | + // The top-level callback is the floor: when Settings is reachable it renders | |
| 751 | + // Settings; when Settings is hidden it falls back to the first reachable | |
| 752 | + // keep-list page so the menu never leads to a blank page. | |
| 753 | + $top_level_callback = self::resolve_top_level_callback($admin); | |
| 650 | 754 | add_menu_page( |
| 651 | 755 | $menu_name, |
| 652 | 756 | $menu_title, |
| 653 | 757 | $menu_capability, |
| 654 | 758 | $menu_slug, |
| 655 | - array($admin, 'create_admin_settings_page'), | |
| 759 | + $top_level_callback, | |
| 656 | 760 | $menu_icon, |
| 657 | 761 | '100.1' |
| 658 | 762 | ); |
| 659 | 763 | |
| @@ -667,114 +771,157 @@ | ||
| 667 | 771 | |
| 668 | 772 | // ── Connect slug ────────────────────────────────────────────────── |
| 669 | 773 | $connect_slug = $menu_slug . '-connect'; |
| 670 | 774 | |
| 671 | - // Dashboard | |
| 672 | - if (Metasync_Access_Control::user_can_access('hide_dashboard')) { | |
| 673 | - add_submenu_page($menu_slug, 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe')); | |
| 775 | + // Keep a slug-to-title map so hidden pages can still provide WordPress | |
| 776 | + // with a browser-tab title when they are registered without a parent. | |
| 777 | + $admin_page_titles = array(); | |
| 778 | + | |
| 779 | + // Local helper: register a submenu page under the plugin parent slug | |
| 780 | + // when the item keeps its WordPress admin sidebar row, or under an | |
| 781 | + // empty parent slug (hidden from the sidebar but still reachable) | |
| 782 | + // otherwise. The Settings page (general) is registered against the | |
| 783 | + // bare $menu_slug so it doubles as the parent menu callback. | |
| 784 | + $register_submenu = function($item_key, $page_title, $menu_title, $capability, $menu_page_slug, $callback) use ($menu_slug, &$admin_page_titles) { | |
| 785 | + $admin_page_titles[$menu_page_slug] = $page_title; | |
| 786 | + $parent = self::item_keeps_wp_sidebar_row($item_key) ? $menu_slug : ''; | |
| 787 | + add_submenu_page($parent, $page_title, $menu_title, $capability, $menu_page_slug, $callback); | |
| 788 | + }; | |
| 789 | + | |
| 790 | + // Dashboard (check access control + "Hide Dashboard" general setting) | |
| 791 | + if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) { | |
| 792 | + $register_submenu('dashboard', 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe')); | |
| 674 | 793 | } |
| 675 | 794 | |
| 676 | 795 | // Indexation Control |
| 677 | 796 | if (Metasync_Access_Control::user_can_access('hide_indexation_control')) { |
| 678 | - add_submenu_page($menu_slug, 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page')); | |
| 797 | + $register_submenu('seo_controls', 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page')); | |
| 679 | 798 | } |
| 680 | 799 | |
| 681 | 800 | // 404 Monitor |
| 682 | - add_submenu_page($menu_slug, '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page')); | |
| 801 | + $register_submenu('monitor_404', '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page')); | |
| 683 | 802 | |
| 684 | 803 | // Redirections |
| 685 | 804 | if (Metasync_Access_Control::user_can_access('hide_redirections')) { |
| 686 | - add_submenu_page($menu_slug, 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page')); | |
| 805 | + $register_submenu('redirections', 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page')); | |
| 687 | 806 | } |
| 688 | 807 | |
| 689 | 808 | // XML Sitemap |
| 690 | 809 | if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) { |
| 691 | - add_submenu_page($menu_slug, 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page')); | |
| 810 | + $register_submenu('xml_sitemap', 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page')); | |
| 692 | 811 | } |
| 693 | 812 | |
| 694 | 813 | // Robots.txt |
| 695 | 814 | if (Metasync_Access_Control::user_can_access('hide_robots')) { |
| 696 | - add_submenu_page($menu_slug, 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page')); | |
| 815 | + $register_submenu('robots_txt', 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page')); | |
| 697 | 816 | } |
| 698 | 817 | |
| 699 | - // Site Verification | |
| 700 | - add_submenu_page($menu_slug, 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page')); | |
| 818 | + // Site Verification (in-page sidebar only in short mode) | |
| 819 | + $register_submenu('site_verification', 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page')); | |
| 701 | 820 | |
| 702 | - // Instant Indexing (conditional) | |
| 703 | - if ($seo_controls['enable_googleinstantindex'] ?? false) { | |
| 704 | - add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page')); | |
| 705 | - } | |
| 706 | - | |
| 707 | 821 | // Google Console (conditional) |
| 708 | 822 | if ($general_options['enable_google_console'] ?? false) { |
| 709 | - add_submenu_page($menu_slug, 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page')); | |
| 823 | + $register_submenu('google_console', 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page')); | |
| 710 | 824 | } |
| 711 | 825 | |
| 712 | 826 | // Bing Console (conditional) |
| 713 | 827 | if ($seo_controls['enable_binginstantindex'] ?? false) { |
| 714 | - add_submenu_page($menu_slug, 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page')); | |
| 828 | + $register_submenu('bing_console', 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page')); | |
| 715 | 829 | } |
| 716 | 830 | |
| 717 | 831 | // Schema Markup |
| 718 | - add_submenu_page($menu_slug, 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page')); | |
| 832 | + $register_submenu('schema_markup', 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page')); | |
| 719 | 833 | |
| 720 | 834 | // Import SEO Data |
| 721 | 835 | if (Metasync_Access_Control::user_can_access('hide_import_seo')) { |
| 722 | - add_submenu_page($menu_slug, 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page')); | |
| 836 | + $register_submenu('import_seo', 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page')); | |
| 723 | 837 | } |
| 724 | 838 | |
| 725 | - // Settings | |
| 839 | + // Settings — registered against the bare $menu_slug (parent menu floor). | |
| 840 | + // It always keeps its sidebar row so the plugin always has at least | |
| 841 | + // one working entry point (see is_settings_reachable()). | |
| 726 | 842 | if (Metasync_Access_Control::user_can_access('hide_settings')) { |
| 727 | - add_submenu_page($menu_slug, 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page')); | |
| 843 | + $register_submenu('general', 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page')); | |
| 728 | 844 | } |
| 729 | 845 | |
| 730 | - // Local Business | |
| 731 | - add_submenu_page($menu_slug, 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page')); | |
| 846 | + // Local Business (in-page sidebar only in short mode) | |
| 847 | + $register_submenu('local_business', 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page')); | |
| 732 | 848 | |
| 733 | - // Breadcrumbs | |
| 734 | - add_submenu_page($menu_slug, 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page')); | |
| 849 | + // Breadcrumbs (in-page sidebar only in short mode) | |
| 850 | + $register_submenu('breadcrumbs', 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page')); | |
| 735 | 851 | |
| 736 | - // Code Snippets | |
| 737 | - add_submenu_page($menu_slug, 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page')); | |
| 852 | + // Code Snippets (in-page sidebar only in short mode) | |
| 853 | + $register_submenu('code_snippets', 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page')); | |
| 738 | 854 | |
| 739 | - // Code Minification | |
| 740 | - add_submenu_page($menu_slug, 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page')); | |
| 855 | + // Code Minification (in-page sidebar only in short mode) | |
| 856 | + $register_submenu('code_minification', 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page')); | |
| 741 | 857 | |
| 858 | + // Media Optimization (in-page sidebar only in short mode) | |
| 859 | + $register_submenu('media_optimization', 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page')); | |
| 860 | + | |
| 742 | 861 | // Custom Pages |
| 743 | 862 | if (Metasync_Access_Control::user_can_access('hide_custom_pages')) { |
| 744 | - add_submenu_page($menu_slug, 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page')); | |
| 863 | + $register_submenu('custom_pages', 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page')); | |
| 745 | 864 | } |
| 746 | 865 | |
| 747 | - // Bot Statistics | |
| 748 | - add_submenu_page($menu_slug, 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page')); | |
| 866 | + // Bot Statistics (in-page sidebar only in short mode) | |
| 867 | + $register_submenu('bot_statistics', 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page')); | |
| 749 | 868 | |
| 750 | 869 | // SEO Health dashboard |
| 751 | - add_submenu_page($menu_slug, 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page')); | |
| 870 | + $register_submenu('seo_health', 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page')); | |
| 752 | 871 | |
| 753 | 872 | // Compatibility |
| 754 | 873 | if (Metasync_Access_Control::user_can_access('hide_compatibility')) { |
| 755 | - add_submenu_page($menu_slug, 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page')); | |
| 874 | + $register_submenu('compatibility', 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page')); | |
| 756 | 875 | } |
| 757 | 876 | |
| 758 | 877 | // Changes Log |
| 759 | 878 | if (Metasync_Access_Control::user_can_access('hide_sync_log')) { |
| 760 | - add_submenu_page($menu_slug, 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page')); | |
| 879 | + $register_submenu('sync_log', 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page')); | |
| 761 | 880 | } |
| 762 | 881 | |
| 763 | 882 | // Report Issue |
| 764 | 883 | if (Metasync_Access_Control::user_can_access('hide_report_issue')) { |
| 765 | - add_submenu_page($menu_slug, 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page')); | |
| 884 | + $register_submenu('report_issue', 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page')); | |
| 766 | 885 | } |
| 767 | 886 | |
| 768 | 887 | // ── Connect CTA (shown when not authenticated) ──────────────────── |
| 888 | + // Kept as a sidebar row so an unconnected site always has a visible | |
| 889 | + // entry point alongside the top-level menu item. | |
| 769 | 890 | if (!$is_fully_connected) { |
| 770 | - add_submenu_page($menu_slug, 'Connect to SearchAtlas', 'Connect to SearchAtlas', $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page')); | |
| 891 | + $connect_label = sprintf('Connect to %s', Metasync::get_effective_plugin_name()); | |
| 892 | + add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page')); | |
| 771 | 893 | } |
| 772 | 894 | |
| 773 | 895 | // ── Hidden pages (no sidebar entry needed) ──────────────────────── |
| 896 | + $admin_page_titles[$menu_slug . '-setup-wizard'] = 'Setup Wizard'; | |
| 774 | 897 | add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page')); |
| 775 | 898 | |
| 899 | + // WordPress cannot resolve titles for pages registered under an empty | |
| 900 | + // parent, even though those pages remain reachable by URL. Restore the | |
| 901 | + // standard admin title format for our registered pages only. | |
| 902 | + add_filter('admin_title', function($admin_title, $title) use (&$admin_page_titles) { | |
| 903 | + $current_page = isset($_GET['page']) && is_string($_GET['page']) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page slug for the admin title | |
| 904 | + ? sanitize_text_field(wp_unslash($_GET['page'])) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page slug for the admin title | |
| 905 | + : ''; | |
| 776 | 906 | |
| 907 | + if (empty($title) && isset($admin_page_titles[$current_page])) { | |
| 908 | + $site_title = is_network_admin() || is_user_admin() | |
| 909 | + ? get_network()->site_name | |
| 910 | + : get_bloginfo('name'); | |
| 911 | + | |
| 912 | + $admin_title = sprintf( | |
| 913 | + /* translators: 1: Admin page title, 2: Site or network title. */ | |
| 914 | + __('%1$s ‹ %2$s — WordPress', 'metasync'), | |
| 915 | + $admin_page_titles[$current_page], | |
| 916 | + $site_title | |
| 917 | + ); | |
| 918 | + } | |
| 919 | + | |
| 920 | + return $admin_title; | |
| 921 | + }, 10, 2); | |
| 922 | + | |
| 923 | + | |
| 777 | 924 | // Rename auto-generated first submenu from plugin name to "Settings" and reorder |
| 778 | 925 | add_action('admin_menu', function() use ($menu_slug) { |
| 779 | 926 | global $submenu; |
| 780 | 927 | if (!isset($submenu[$menu_slug])) { |
| @@ -804,9 +951,8 @@ | ||
| 804 | 951 | 'dashboard' => 'dashboard', |
| 805 | 952 | 'compatibility' => 'admin-tools', |
| 806 | 953 | 'sync_log' => 'list-view', |
| 807 | 954 | 'seo_controls' => 'search', |
| 808 | - 'optimal_settings' => 'superhero-alt', | |
| 809 | 955 | 'instant_index' => 'performance', |
| 810 | 956 | 'google_console' => 'chart-area', |
| 811 | 957 | 'bing_console' => 'chart-bar', |
| 812 | 958 | 'redirections' => 'undo', |
| @@ -846,9 +992,9 @@ | ||
| 846 | 992 | <?php |
| 847 | 993 | // Dashboard tab (standalone) |
| 848 | 994 | if (isset($seo_items['dashboard'])) { |
| 849 | 995 | $is_active = ($current_page === 'dashboard'); |
| 850 | - $icon = $menu_icons['dashboard'] ?? 'admin-generic'; | |
| 996 | + $icon = $menu_icons['dashboard']; | |
| 851 | 997 | $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix']; |
| 852 | 998 | ?> |
| 853 | 999 | <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>"> |
| 854 | 1000 | <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span> |
| @@ -1195,13 +1341,13 @@ | ||
| 1195 | 1341 | <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" /> |
| 1196 | 1342 | </div> |
| 1197 | 1343 | <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?> |
| 1198 | 1344 | <div class="metasync-logo-container"> |
| 1199 | - <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo" /> | |
| 1345 | + <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" /> | |
| 1200 | 1346 | </div> |
| 1201 | 1347 | <?php endif; ?> |
| 1202 | 1348 | </div> |
| 1203 | - | |
| 1349 | + | |
| 1204 | 1350 | <div class="metasync-header-right"> |
| 1205 | 1351 | <!-- Theme Toggle --> |
| 1206 | 1352 | <div class="metasync-theme-toggle" role="group" aria-label="Theme Selector"> |
| 1207 | 1353 | <button class="metasync-theme-option <?php echo ($current_theme === 'light') ? 'active' : ''; ?>" data-theme="light" aria-label="Light Theme" type="button"> |
| @@ -1311,10 +1457,10 @@ | ||
| 1311 | 1457 | |
| 1312 | 1458 | <?php |
| 1313 | 1459 | $page_slug = Metasync_Admin::$page_slug; |
| 1314 | 1460 | $is_plugin_page = ( |
| 1315 | - (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) || | |
| 1316 | - (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false) | |
| 1461 | + (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for an inline config script | |
| 1462 | + (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for an inline config script | |
| 1317 | 1463 | ); |
| 1318 | 1464 | if ($is_plugin_page): |
| 1319 | 1465 | ?> |
| 1320 | 1466 | <script type="text/javascript"> |
| @@ -1405,64 +1551,78 @@ | ||
| 1405 | 1551 | # For backward compatibility, constant takes precedence. |
| 1406 | 1552 | if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) { |
| 1407 | 1553 | return; |
| 1408 | 1554 | } |
| 1409 | - | |
| 1555 | + | |
| 1410 | 1556 | # Check if admin bar status is disabled via setting |
| 1411 | 1557 | $general_settings = Metasync::get_option('general'); |
| 1558 | + if (!is_array($general_settings)) { | |
| 1559 | + $general_settings = []; | |
| 1560 | + } | |
| 1412 | 1561 | $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true; |
| 1413 | 1562 | if (!$show_admin_bar) { |
| 1414 | 1563 | return; |
| 1415 | 1564 | } |
| 1416 | - | |
| 1417 | - if (!Metasync::current_user_has_plugin_access()) { | |
| 1418 | - return; | |
| 1419 | - } | |
| 1420 | 1565 | |
| 1421 | 1566 | if (!is_admin() && !apply_filters('metasync_show_admin_bar_status_frontend', false)) { |
| 1422 | 1567 | return; |
| 1423 | 1568 | } |
| 1424 | 1569 | |
| 1425 | - $general_settings = Metasync::get_option('general'); | |
| 1426 | - if (!is_array($general_settings)) { | |
| 1427 | - $general_settings = []; | |
| 1428 | - } | |
| 1429 | - | |
| 1430 | - $is_synced = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings); | |
| 1431 | - | |
| 1432 | - $plugin_name = Metasync::get_effective_plugin_name(); | |
| 1433 | - | |
| 1434 | - if ($is_synced) { | |
| 1435 | - $otto_uuid = $general_settings['otto_pixel_uuid'] ?? ''; | |
| 1436 | - if (!empty($otto_uuid)) { | |
| 1437 | - $status_emoji = '🟢'; // Green circle for synced | |
| 1438 | - $title = $plugin_name . ' - Synced (Heartbeat API connectivity verified)'; | |
| 1439 | - $status_class = 'searchatlas-synced'; | |
| 1570 | + $node = get_transient(self::CACHE_KEY); | |
| 1571 | + | |
| 1572 | + if ($node === false) { | |
| 1573 | + $is_synced = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings); | |
| 1574 | + | |
| 1575 | + $plugin_name = Metasync::get_effective_plugin_name(); | |
| 1576 | + | |
| 1577 | + if ($is_synced) { | |
| 1578 | + $otto_uuid = $general_settings['otto_pixel_uuid'] ?? ''; | |
| 1579 | + if (!empty($otto_uuid)) { | |
| 1580 | + $status_emoji = '🟢'; // Green circle for synced | |
| 1581 | + $title = $plugin_name . ' - Synced (Heartbeat API connectivity verified)'; | |
| 1582 | + $status_class = 'searchatlas-synced'; | |
| 1583 | + } else { | |
| 1584 | + $status_emoji = '🟡'; // Yellow circle for warning | |
| 1585 | + $title = $plugin_name . ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.'; | |
| 1586 | + $status_class = 'searchatlas-warning'; | |
| 1587 | + } | |
| 1440 | 1588 | } else { |
| 1441 | - $status_emoji = '🟡'; // Yellow circle for warning | |
| 1442 | - $title = $plugin_name . ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.'; | |
| 1443 | - $status_class = 'searchatlas-warning'; | |
| 1589 | + $status_emoji = '🔴'; // Red circle for not synced | |
| 1590 | + $title = $plugin_name . ' - Not Synced (Heartbeat API not responding or unreachable)'; | |
| 1591 | + $status_class = 'searchatlas-not-synced'; | |
| 1444 | 1592 | } |
| 1445 | - } else { | |
| 1446 | - $status_emoji = '🔴'; // Red circle for not synced | |
| 1447 | - $title = $plugin_name . ' - Not Synced (Heartbeat API not responding or unreachable)'; | |
| 1448 | - $status_class = 'searchatlas-not-synced'; | |
| 1593 | + | |
| 1594 | + $admin_bar_title = $plugin_name . ' ' . $status_emoji; | |
| 1595 | + | |
| 1596 | + $node = array( | |
| 1597 | + 'title' => $admin_bar_title, | |
| 1598 | + 'href' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug), | |
| 1599 | + 'meta_title' => $title, | |
| 1600 | + 'meta_class' => $status_class, | |
| 1601 | + ); | |
| 1602 | + | |
| 1603 | + set_transient(self::CACHE_KEY, $node, apply_filters('metasync_admin_bar_status_cache_ttl', 5 * MINUTE_IN_SECONDS)); | |
| 1449 | 1604 | } |
| 1450 | 1605 | |
| 1451 | - $display_name = $plugin_name; | |
| 1452 | - $admin_bar_title = $display_name . ' ' . $status_emoji; | |
| 1453 | - | |
| 1454 | 1606 | $wp_admin_bar->add_node(array( |
| 1455 | 1607 | 'id' => 'searchatlas-status', |
| 1456 | - 'title' => $admin_bar_title, | |
| 1457 | - 'href' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug), | |
| 1608 | + 'title' => $node['title'], | |
| 1609 | + 'href' => $node['href'], | |
| 1458 | 1610 | 'meta' => array( |
| 1459 | - 'title' => $title, | |
| 1460 | - 'class' => $status_class | |
| 1461 | - ) | |
| 1611 | + 'title' => $node['meta_title'], | |
| 1612 | + 'class' => $node['meta_class'], | |
| 1613 | + ), | |
| 1462 | 1614 | )); |
| 1463 | 1615 | } |
| 1464 | 1616 | |
| 1617 | + /** | |
| 1618 | + * Invalidate the admin bar status cache so the next page load recomputes status. | |
| 1619 | + */ | |
| 1620 | + public static function invalidate_admin_bar_status_cache() | |
| 1621 | + { | |
| 1622 | + delete_transient(self::CACHE_KEY); | |
| 1623 | + } | |
| 1624 | + | |
| 1465 | 1625 | // ------------------------------------------------------------------ |
| 1466 | 1626 | // Yoast-style 3-column layout helpers |
| 1467 | 1627 | // ------------------------------------------------------------------ |
| 1468 | 1628 | |
| @@ -1481,12 +1641,14 @@ | ||
| 1481 | 1641 | $theme = esc_attr(get_option('metasync_theme', 'dark')); |
| 1482 | 1642 | $plugin_name = Metasync::get_effective_plugin_name(); |
| 1483 | 1643 | $general = Metasync::get_option('general') ?? []; |
| 1484 | 1644 | $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general); |
| 1645 | + // badge reflects confirmed heartbeat health (connected / stale / | |
| 1646 | + // disconnected), not raw API-key presence. | |
| 1647 | + $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general); | |
| 1485 | 1648 | $logo = $this->resolve_logo_data(); |
| 1486 | 1649 | |
| 1487 | 1650 | $current_theme = get_option('metasync_theme', 'dark'); |
| 1488 | - $api_key = $general['searchatlas_api_key'] ?? ''; | |
| 1489 | 1651 | ?> |
| 1490 | 1652 | <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo $theme; ?>"> |
| 1491 | 1653 | |
| 1492 | 1654 | <?php |
| @@ -1533,17 +1695,17 @@ | ||
| 1533 | 1695 | <?php if ($logo['show_logo'] && $logo['use_dual']): ?> |
| 1534 | 1696 | <img src="<?php echo esc_url($logo['light_url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo metasync-logo-light" style="height:28px;width:auto;"> |
| 1535 | 1697 | <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo metasync-logo-dark" style="height:28px;width:auto;"> |
| 1536 | 1698 | <?php elseif ($logo['show_logo'] && $logo['url']): ?> |
| 1537 | - <img src="<?php echo esc_url($logo['url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo" style="height:28px;width:auto;"> | |
| 1699 | + <img src="<?php echo esc_url($logo['url']); ?>" alt="<?php echo esc_attr($plugin_name); ?>" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" style="height:28px;width:auto;"> | |
| 1538 | 1700 | <?php else: ?> |
| 1539 | 1701 | <strong style="font-size:15px;color:var(--dashboard-text-primary);"><?php echo esc_html($plugin_name); ?></strong> |
| 1540 | 1702 | <?php endif; ?> |
| 1541 | 1703 | </div> |
| 1542 | 1704 | <div class="metasync-header-compact-right"> |
| 1543 | - <div class="metasync-status <?php echo !empty($api_key) ? 'connected' : 'disconnected'; ?>"> | |
| 1705 | + <div class="metasync-status <?php echo esc_attr($badge['class']); ?>"> | |
| 1544 | 1706 | <span class="status-dot"></span> |
| 1545 | - <span class="status-text"><?php echo !empty($api_key) ? 'Connected' : 'Not Connected'; ?></span> | |
| 1707 | + <span class="status-text"><?php echo esc_html($badge['text']); ?></span> | |
| 1546 | 1708 | </div> |
| 1547 | 1709 | <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme"> |
| 1548 | 1710 | <span class="theme-icon-light">☀</span> |
| 1549 | 1711 | <span class="theme-icon-dark">☾</span> |
| @@ -1560,8 +1722,17 @@ | ||
| 1560 | 1722 | </aside> |
| 1561 | 1723 | |
| 1562 | 1724 | <!-- Main content --> |
| 1563 | 1725 | <main class="metasync-layout-main"> |
| 1726 | + <?php | |
| 1727 | + // Anchor for WP core's notice relocation (common.js). Without it, core | |
| 1728 | + // moves admin notices after the first .wrap h1 only on DOM-ready, after | |
| 1729 | + // the server has already painted them at the top of #wpbody-content — | |
| 1730 | + // causing a flash above the header and layout shift. Providing | |
| 1731 | + // an explicit .wp-header-end inside this (FOUC-hidden) wrap makes core | |
| 1732 | + // relocate notices here deterministically, so they fade in with the page. | |
| 1733 | + ?> | |
| 1734 | + <hr class="wp-header-end"> | |
| 1564 | 1735 | <?php if ($page_title || $description): ?> |
| 1565 | 1736 | <div class="metasync-page-header"> |
| 1566 | 1737 | <?php if ($page_title): ?> |
| 1567 | 1738 | <h1><?php echo esc_html($page_title); ?></h1> |
| @@ -1585,15 +1756,19 @@ | ||
| 1585 | 1756 | { |
| 1586 | 1757 | ?> |
| 1587 | 1758 | </main><!-- /.metasync-layout-main --> |
| 1588 | 1759 | |
| 1589 | - <?php if ($show_promo): | |
| 1590 | - $general = Metasync::get_option('general') ?? []; | |
| 1591 | - $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general); | |
| 1760 | + <?php | |
| 1761 | + // The sidebar exists to get an unconnected site connected. Once the | |
| 1762 | + // site is linked it has nothing further to offer, so the whole | |
| 1763 | + // column is dropped and the grid reflows to two columns. | |
| 1764 | + $show_promo = $show_promo && ! Metasync_Heartbeat_Manager::instance() | |
| 1765 | + ->is_heartbeat_connected(Metasync::get_option('general') ?? []); | |
| 1766 | + if ($show_promo): | |
| 1592 | 1767 | ?> |
| 1593 | 1768 | <!-- Right promo sidebar --> |
| 1594 | 1769 | <aside class="metasync-layout-promo"> |
| 1595 | - <?php $this->render_promo_sidebar($is_connected); ?> | |
| 1770 | + <?php $this->render_promo_sidebar(); ?> | |
| 1596 | 1771 | </aside> |
| 1597 | 1772 | <?php endif; ?> |
| 1598 | 1773 | |
| 1599 | 1774 | </div><!-- /.metasync-layout --> |
| @@ -1630,9 +1805,8 @@ | ||
| 1630 | 1805 | 'breadcrumbs' => 'menu', |
| 1631 | 1806 | 'code_snippets' => 'editor-code', |
| 1632 | 1807 | 'code_minification'=> 'media-code', |
| 1633 | 1808 | 'custom_pages' => 'admin-page', |
| 1634 | - 'optimal_settings' => 'superhero-alt', | |
| 1635 | 1809 | 'compatibility' => 'admin-tools', |
| 1636 | 1810 | 'sync_log' => 'list-view', |
| 1637 | 1811 | 'bot_statistics' => 'visibility', |
| 1638 | 1812 | 'report_issue' => 'sos', |
| @@ -1648,11 +1822,20 @@ | ||
| 1648 | 1822 | } else { |
| 1649 | 1823 | $plugin_items[$key] = $item; |
| 1650 | 1824 | } |
| 1651 | 1825 | } |
| 1826 | + | |
| 1827 | + $total_items = count($seo_items) + count($plugin_items); | |
| 1828 | + | |
| 1652 | 1829 | ?> |
| 1653 | 1830 | <nav class="metasync-sidenav"> |
| 1654 | 1831 | |
| 1832 | + <?php if ($total_items === 0): ?> | |
| 1833 | + <div class="metasync-sidenav-empty"> | |
| 1834 | + <?php esc_html_e('No pages are available.', 'metasync'); ?> | |
| 1835 | + </div> | |
| 1836 | + <?php else: ?> | |
| 1837 | + | |
| 1655 | 1838 | <!-- SEO Features group --> |
| 1656 | 1839 | <div class="metasync-sidenav-group"> |
| 1657 | 1840 | <div class="metasync-sidenav-group-title">SEO Features</div> |
| 1658 | 1841 | <ul> |
| @@ -1718,8 +1901,10 @@ | ||
| 1718 | 1901 | </ul> |
| 1719 | 1902 | </div> |
| 1720 | 1903 | <?php endif; ?> |
| 1721 | 1904 | |
| 1905 | + <?php endif; ?> | |
| 1906 | + | |
| 1722 | 1907 | </nav> |
| 1723 | 1908 | <?php |
| 1724 | 1909 | } |
| 1725 | 1910 | |
| @@ -1725,17 +1910,22 @@ | ||
| 1725 | 1910 | |
| 1726 | 1911 | /** |
| 1727 | 1912 | * Render the right promotional sidebar. |
| 1728 | 1913 | * |
| 1729 | - * @param bool $is_connected Whether the plugin is authenticated. | |
| 1914 | + * Only reached while the site is unconnected; the caller drops the | |
| 1915 | + * whole column once the plugin is linked. | |
| 1730 | 1916 | */ |
| 1731 | - public function render_promo_sidebar($is_connected = false) | |
| 1917 | + public function render_promo_sidebar() | |
| 1732 | 1918 | { |
| 1733 | 1919 | $plugin_name = Metasync::get_effective_plugin_name(); |
| 1734 | 1920 | $otto_name = Metasync::get_whitelabel_otto_name(); |
| 1735 | 1921 | $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug)); |
| 1736 | 1922 | $homepage = Metasync::HOMEPAGE_DOMAIN; |
| 1737 | - $is_default_brand = ( $plugin_name === 'Search Atlas' ); | |
| 1923 | + $docs_url = Metasync::DOCUMENTATION_DOMAIN; | |
| 1924 | + // These links point at Search Atlas properties, so they only belong | |
| 1925 | + // here while the plugin runs under its own name AND branding — a | |
| 1926 | + // reseller can leave the name untouched and still be whitelabeled. | |
| 1927 | + $is_default_brand = ( $plugin_name === 'Search Atlas' ) && ! Metasync::is_whitelabel_enabled(); | |
| 1738 | 1928 | $whitelabel = Metasync::get_whitelabel_settings(); |
| 1739 | 1929 | $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links']) |
| 1740 | 1930 | ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); }) |
| 1741 | 1931 | : []; |
| @@ -1740,9 +1930,8 @@ | ||
| 1740 | 1930 | ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); }) |
| 1741 | 1931 | : []; |
| 1742 | 1932 | ?> |
| 1743 | 1933 | |
| 1744 | - <?php if (!$is_connected): ?> | |
| 1745 | 1934 | <!-- Connect CTA card --> |
| 1746 | 1935 | <div class="metasync-promo-card metasync-promo-card--connect"> |
| 1747 | 1936 | <div class="metasync-promo-card-header"> |
| 1748 | 1937 | <div class="metasync-promo-card-icon"> |
| @@ -1762,36 +1951,8 @@ | ||
| 1762 | 1951 | <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary"> |
| 1763 | 1952 | Connect Now |
| 1764 | 1953 | </a> |
| 1765 | 1954 | </div> |
| 1766 | - <?php else: ?> | |
| 1767 | - <!-- Connected — feature highlights --> | |
| 1768 | - <div class="metasync-promo-card metasync-promo-card--accent"> | |
| 1769 | - <div class="metasync-promo-card-header"> | |
| 1770 | - <div class="metasync-promo-card-icon"> | |
| 1771 | - <span class="dashicons dashicons-performance"></span> | |
| 1772 | - </div> | |
| 1773 | - <div> | |
| 1774 | - <h3><?php echo esc_html($otto_name); ?> Active</h3> | |
| 1775 | - </div> | |
| 1776 | - </div> | |
| 1777 | - <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p> | |
| 1778 | - <ul class="metasync-promo-benefits"> | |
| 1779 | - <li><span class="promo-check">✓</span> Schema markup auto-applied</li> | |
| 1780 | - <li><span class="promo-check">✓</span> Meta titles & descriptions optimized</li> | |
| 1781 | - <li><span class="promo-check">✓</span> Internal linking suggestions active</li> | |
| 1782 | - </ul> | |
| 1783 | - <?php if ($is_default_brand): ?> | |
| 1784 | - <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline"> | |
| 1785 | - View <?php echo esc_html($plugin_name); ?> Dashboard | |
| 1786 | - </a> | |
| 1787 | - <?php elseif (!empty($whitelabel['domain'])): ?> | |
| 1788 | - <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline"> | |
| 1789 | - View <?php echo esc_html($plugin_name); ?> Dashboard | |
| 1790 | - </a> | |
| 1791 | - <?php endif; ?> | |
| 1792 | - </div> | |
| 1793 | - <?php endif; ?> | |
| 1794 | 1955 | |
| 1795 | 1956 | <?php |
| 1796 | 1957 | // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none |
| 1797 | 1958 | $show_quick_links = $is_default_brand || !empty($custom_links); |
| @@ -1801,12 +1962,20 @@ | ||
| 1801 | 1962 | <div class="metasync-promo-card"> |
| 1802 | 1963 | <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3> |
| 1803 | 1964 | <ul class="metasync-promo-links"> |
| 1804 | 1965 | <?php if ($is_default_brand): ?> |
| 1966 | + <li><a href="<?php echo esc_url($docs_url); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-book-alt"></span> Documentation</a></li> | |
| 1805 | 1967 | <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li> |
| 1806 | - <li><a href="<?php echo esc_url($homepage . '/academy/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-welcome-learn-more"></span> SEO Academy</a></li> | |
| 1968 | + <li><a href="<?php echo esc_url($homepage . '/contact/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-email-alt"></span> Contact Support</a></li> | |
| 1807 | 1969 | <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-setup-wizard')); ?>"><span class="dashicons dashicons-admin-customizer"></span> Setup Wizard</a></li> |
| 1970 | + <?php | |
| 1971 | + // Mirrors the gate that decides whether the page is registered | |
| 1972 | + // at all. Without it the link outlives the page for any user | |
| 1973 | + // the access rules hide it from, and leads nowhere. | |
| 1974 | + if (Metasync_Access_Control::user_can_access('hide_report_issue')): | |
| 1975 | + ?> | |
| 1808 | 1976 | <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-report-issue')); ?>"><span class="dashicons dashicons-sos"></span> Report Issue</a></li> |
| 1977 | + <?php endif; ?> | |
| 1809 | 1978 | <?php else: ?> |
| 1810 | 1979 | <?php foreach ($custom_links as $link): ?> |
| 1811 | 1980 | <li><a href="<?php echo esc_url($link['url']); ?>" <?php echo !empty($link['external']) ? 'target="_blank" rel="noopener"' : ''; ?>><span class="dashicons dashicons-admin-links"></span> <?php echo esc_html($link['label'] ?: $link['url']); ?></a></li> |
| 1812 | 1981 | <?php endforeach; ?> |