PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.18
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.18
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-admin-navigation.php

class-metasync-admin-navigation.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.18, at includes/class-metasync-admin-navigation.php

1,887 lines 96.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * Admin navigation header, nav tabs, and admin-bar status indicator.
8 *
9 * Extracted from Metasync_Admin to keep the admin class focused on
10 * non-UI concerns. All header/nav rendering and admin-bar badge
11 * logic lives here.
12 *
13 * @package Metasync
14 * @subpackage Metasync/includes
15 */
16 class Metasync_Admin_Navigation
17 {
18 /** @var self|null */
19 private static $instance = null;
20
21 const CACHE_KEY = 'metasync_admin_bar_status';
22
23 /**
24 * Get singleton instance.
25 *
26 * @return self
27 */
28 public static function instance()
29 {
30 if (self::$instance === null) {
31 self::$instance = new self();
32 }
33 return self::$instance;
34 }
35
36 private function __construct() {}
37
38 /**
39 * Whether the Dashboard is hidden by the "Hide Dashboard" general setting
40 * (hide_dashboard_framework).
41 *
42 * When enabled this removes the Dashboard from the WordPress admin submenu
43 * and from the in-plugin SEO feature navigation, matching the setting's
44 * description "Hide the main dashboard from the WordPress admin menu".
45 *
46 * @return bool
47 */
48 public static function is_dashboard_hidden_by_framework()
49 {
50 $general_options = Metasync::get_option('general') ?? [];
51 return filter_var($general_options['hide_dashboard_framework'] ?? false, FILTER_VALIDATE_BOOLEAN);
52 }
53
54 // ------------------------------------------------------------------
55 // Logo resolution helper
56 // ------------------------------------------------------------------
57
58 /**
59 * Resolve which logo(s) to display based on whitelabel settings.
60 *
61 * @return array{show_logo: bool, use_dual: bool, url: string, light_url: string, dark_url: string, is_default: bool}
62 */
63 private function resolve_logo_data()
64 {
65 $wl_settings = Metasync::get_whitelabel_settings();
66 $is_whitelabel = !empty($wl_settings['is_whitelabel']);
67
68 $light_raw = Metasync::get_whitelabel_logo_light();
69 $dark_raw = Metasync::get_whitelabel_logo_dark();
70 $has_light = !empty($light_raw) && filter_var($light_raw, FILTER_VALIDATE_URL);
71 $has_dark = !empty($dark_raw) && filter_var($dark_raw, FILTER_VALIDATE_URL);
72 $use_dual = ($has_light && $has_dark && $light_raw !== $dark_raw);
73
74 $data = [
75 'show_logo' => false,
76 'use_dual' => false,
77 'url' => '',
78 'light_url' => '',
79 'dark_url' => '',
80 'is_default' => false,
81 ];
82
83 if ($use_dual) {
84 $data['show_logo'] = true;
85 $data['use_dual'] = true;
86 $data['light_url'] = $light_raw;
87 $data['dark_url'] = $dark_raw;
88 } else {
89 $single = $has_light ? $light_raw : ($has_dark ? $dark_raw : null);
90 if ($single) {
91 $data['show_logo'] = true;
92 $data['url'] = $single;
93 } elseif (!$is_whitelabel) {
94 $data['show_logo'] = true;
95 $data['url'] = plugin_dir_url(dirname(__FILE__)) . 'assets/images/searchatlas-logo.svg';
96 $data['is_default'] = true;
97 }
98 }
99
100 return $data;
101 }
102
103 // ------------------------------------------------------------------
104 // Header + Nav rendering
105 // ------------------------------------------------------------------
106
107 /**
108 * Render the standard header followed by the navigation tabs.
109 */
110 public function render_standard_header_nav($page_title = null, $current_page = null)
111 {
112 $this->render_static_header($page_title);
113 $this->render_static_navigation($current_page);
114 }
115
116 /**
117 * Render the page header (logo, connection badge, theme toggle).
118 */
119 public function render_static_header($page_title = null)
120 {
121 $effective_plugin_name = Metasync::get_effective_plugin_name();
122 $display_title = $page_title ?: $effective_plugin_name;
123 $logo = $this->resolve_logo_data();
124
125 $current_theme = get_option('metasync_theme', 'dark');
126 $general_settings = Metasync::get_option('general');
127 // derive the badge from confirmed heartbeat health, not from
128 // mere API-key presence (which stays set after Search Atlas revokes it).
129 $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general_settings);
130 ?>
131 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
132 <div class="metasync-header-left">
133 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
134 <div class="metasync-logo-container">
135 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-light" />
136 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
137 </div>
138 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
139 <div class="metasync-logo-container">
140 <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
141 </div>
142 <?php endif; ?>
143 </div>
144 <div class="metasync-header-right">
145 <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
146 <span class="status-dot"></span>
147 <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
148 </div>
149 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
150 <span class="theme-icon-light">&#9728;</span>
151 <span class="theme-icon-dark">&#9790;</span>
152 </button>
153 </div>
154 </div>
155 <?php
156 }
157
158 /**
159 * Render the horizontal navigation tabs.
160 */
161 public function render_static_navigation($current_page = null)
162 {
163 $general_options = Metasync::get_option('general') ?? [];
164 $whitelabel_settings = Metasync::get_whitelabel_settings();
165 $page_slug = Metasync_Admin::$page_slug;
166
167 $menu_items = [];
168 $menu_icons = [
169 'dashboard' => 'dashboard',
170 'seo_controls' => 'search',
171 'optimal_settings' => 'superhero-alt',
172 'instant_index' => 'performance',
173 'google_console' => 'chart-area',
174 'compatibility' => 'admin-tools',
175 'sync_log' => 'list-view',
176 'redirections' => 'undo',
177 'robots_txt' => 'shield',
178 'xml_sitemap' => 'networking',
179 'custom_pages' => 'admin-page',
180 'report_issue' => 'sos',
181 'general' => 'admin-settings',
182 ];
183
184 if (empty($whitelabel_settings['hide_dashboard']) && !self::is_dashboard_hidden_by_framework()) {
185 $menu_items['dashboard'] = ['title' => 'Dashboard', 'slug_suffix' => '-dashboard'];
186 }
187
188 if (empty($whitelabel_settings['hide_indexation_control'])) {
189 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
190 }
191
192 if ($general_options['enable_optimal_settings'] ?? false) {
193 $menu_items['optimal_settings'] = ['title' => 'Optimal Settings', 'slug_suffix' => '-optimal-settings'];
194 }
195
196 if ($general_options['enable_googleinstantindex'] ?? false) {
197 $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
198 }
199
200 if ($general_options['enable_google_console'] ?? false) {
201 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
202 }
203
204 if (empty($whitelabel_settings['hide_compatibility'])) {
205 $menu_items['compatibility'] = ['title' => 'Compatibility', 'slug_suffix' => '-compatibility'];
206 }
207
208 if (empty($whitelabel_settings['hide_sync_log'])) {
209 $menu_items['sync_log'] = ['title' => 'Changes Log', 'slug_suffix' => '-sync-log'];
210 }
211
212 if (empty($whitelabel_settings['hide_redirections'])) {
213 $menu_items['redirections'] = ['title' => 'Redirections', 'slug_suffix' => '-redirections'];
214 }
215
216 if (empty($whitelabel_settings['hide_robots'])) {
217 $menu_items['robots_txt'] = ['title' => 'Robots.txt', 'slug_suffix' => '-robots-txt'];
218 }
219
220 $menu_items['xml_sitemap'] = ['title' => 'XML Sitemap', 'slug_suffix' => '-xml-sitemap'];
221 ?>
222 <div class="metasync-nav-wrapper">
223 <div class="metasync-nav-tabs">
224 <div class="metasync-nav-left">
225 <?php foreach ($menu_items as $key => $menu_item):
226 $is_active = ($current_page === $key);
227 $icon = $menu_icons[$key] ?? 'admin-generic';
228 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
229 ?>
230 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
231 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
232 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
233 </a>
234 <?php endforeach; ?>
235 </div>
236 <div class="metasync-nav-right">
237 <a href="?page=<?php echo esc_attr( $page_slug ); ?>-custom-pages" class="metasync-nav-tab <?php echo $current_page === 'custom_pages' ? 'active' : ''; ?>" style="margin-right: 10px;">
238 <span class="tab-icon"><span class="dashicons dashicons-admin-page"></span></span>
239 <span class="tab-text">Custom Pages</span>
240 </a>
241 <a href="?page=<?php echo esc_attr( $page_slug ); ?>-report-issue" class="metasync-nav-tab <?php echo $current_page === 'report_issue' ? 'active' : ''; ?>">
242 <span class="tab-icon"><span class="dashicons dashicons-sos"></span></span>
243 <span class="tab-text">Report Issue</span>
244 </a>
245 <div class="metasync-simple-dropdown">
246 <button type="button" class="metasync-seo-btn" id="metasync-seo-btn" onclick="toggleSeoMenuPortal(event)" aria-expanded="false">
247 <span class="tab-icon"><span class="dashicons dashicons-search"></span></span>
248 <span class="tab-text">SEO</span>
249 <span class="dropdown-arrow"></span>
250 </button>
251 </div>
252 <div class="metasync-simple-dropdown">
253 <button type="button" class="metasync-settings-btn" id="metasync-settings-btn" onclick="toggleSettingsMenuPortal(event)" aria-expanded="false">
254 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
255 <span class="tab-text">Settings</span>
256 <span class="dropdown-arrow"></span>
257 </button>
258 </div>
259 </div>
260 </div>
261 </div>
262
263 <script>
264 function toggleSeoMenuPortal(event) {
265 event.preventDefault();
266 event.stopPropagation();
267 var button = event.currentTarget;
268 var existingMenu = document.getElementById('metasync-seo-portal-menu');
269 if (existingMenu) {
270 existingMenu.remove();
271 button.classList.remove('active');
272 button.setAttribute('aria-expanded', 'false');
273 return;
274 }
275 var menu = document.createElement('div');
276 menu.id = 'metasync-seo-portal-menu';
277 menu.className = 'metasync-portal-menu';
278
279 var pageSlug = '<?php echo esc_js( $page_slug ); ?>';
280 var seoLinks = [
281 { href: '?page=' + pageSlug + '-seo-controls', text: 'Indexation Control' },
282 { href: '?page=' + pageSlug + '-xml-sitemap', text: 'XML Sitemap' },
283 { href: '?page=' + pageSlug + '-robots-txt', text: 'Robots.txt' },
284 { href: '?page=' + pageSlug + '-redirections', text: 'Redirections' },
285 ];
286 seoLinks.forEach(function(item) {
287 var link = document.createElement('a');
288 link.href = item.href;
289 link.className = 'metasync-portal-item';
290 link.textContent = item.text;
291 menu.appendChild(link);
292 });
293
294 var rect = button.getBoundingClientRect();
295 menu.style.position = 'fixed';
296 menu.style.top = (rect.bottom + 8) + 'px';
297 menu.style.right = (window.innerWidth - rect.right) + 'px';
298 menu.style.zIndex = '999999999';
299 document.body.appendChild(menu);
300 button.classList.add('active');
301 button.setAttribute('aria-expanded', 'true');
302 }
303
304 function toggleSettingsMenuPortal(event) {
305 event.preventDefault();
306 event.stopPropagation();
307 var button = event.currentTarget;
308 var existingMenu = document.getElementById('metasync-portal-menu');
309 if (existingMenu) {
310 existingMenu.remove();
311 button.classList.remove('active');
312 button.setAttribute('aria-expanded', 'false');
313 return;
314 }
315 var menu = document.createElement('div');
316 menu.id = 'metasync-portal-menu';
317 menu.className = 'metasync-portal-menu';
318
319 var hideAdvanced = <?php echo !empty($whitelabel_settings['hide_advanced']) ? 'true' : 'false'; ?>;
320 var showGeneral = <?php echo Metasync_Access_Control::user_can_access('hide_settings') ? 'true' : 'false'; ?>;
321
322 if (showGeneral) {
323 var generalLink = document.createElement('a');
324 generalLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=general';
325 generalLink.className = 'metasync-portal-item';
326 generalLink.textContent = 'General';
327 menu.appendChild(generalLink);
328 }
329
330 var whitelabelLink = document.createElement('a');
331 whitelabelLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=whitelabel';
332 whitelabelLink.className = 'metasync-portal-item';
333 whitelabelLink.textContent = 'White Label';
334 menu.appendChild(whitelabelLink);
335
336 if (!hideAdvanced) {
337 var advancedLink = document.createElement('a');
338 advancedLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=advanced';
339 advancedLink.className = 'metasync-portal-item';
340 advancedLink.textContent = 'Advanced';
341 menu.appendChild(advancedLink);
342 }
343
344
345 var rect = button.getBoundingClientRect();
346 menu.style.position = 'fixed';
347 menu.style.top = (rect.bottom + 8) + 'px';
348 menu.style.right = (window.innerWidth - rect.right) + 'px';
349 menu.style.zIndex = '999999999';
350 document.body.appendChild(menu);
351 button.classList.add('active');
352 button.setAttribute('aria-expanded', 'true');
353 }
354 document.addEventListener('click', function(event) {
355 var seoButton = document.getElementById('metasync-seo-btn');
356 var seoMenu = document.getElementById('metasync-seo-portal-menu');
357 if (seoMenu && seoButton && !seoButton.contains(event.target) && !seoMenu.contains(event.target)) {
358 seoMenu.remove();
359 seoButton.classList.remove('active');
360 seoButton.setAttribute('aria-expanded', 'false');
361 }
362
363 var button = document.getElementById('metasync-settings-btn');
364 var menu = document.getElementById('metasync-portal-menu');
365 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
366 menu.remove();
367 button.classList.remove('active');
368 button.setAttribute('aria-expanded', 'false');
369 }
370 });
371 </script>
372 <?php
373 }
374
375 // ------------------------------------------------------------------
376 // Menu registration + enhanced navigation (moved from Metasync_Admin)
377 // ------------------------------------------------------------------
378
379 /**
380 * Helper method to get available menu items based on configuration.
381 * Items are grouped into 'seo' (SEO Features) and 'plugin' (Plugin) categories.
382 */
383 public function get_available_menu_items()
384 {
385 $general_options = Metasync::get_option('general') ?? [];
386 $has_api_key = !empty($general_options['searchatlas_api_key'] ?? '');
387 $has_uuid = !empty($general_options['otto_pixel_uuid'] ?? '');
388 $is_fully_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_options);
389
390 $menu_items = [];
391
392 // === SEO FEATURES GROUP ===
393
394 // Dashboard (check access control + "Hide Dashboard" general setting)
395 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
396 $menu_items['dashboard'] = [
397 'title' => 'Dashboard',
398 'slug_suffix' => '-dashboard',
399 'callback' => 'create_admin_dashboard_iframe',
400 'internal_nav' => 'Dashboard',
401 'group' => 'seo'
402 ];
403 }
404
405 // Indexation Control (check access control)
406 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
407 $menu_items['seo_controls'] = [
408 'title' => 'Indexation',
409 'slug_suffix' => '-seo-controls',
410 'callback' => 'create_admin_seo_controls_page',
411 'internal_nav' => 'Indexation Control',
412 'group' => 'seo'
413 ];
414 }
415
416 // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
417 $seo_controls = Metasync::get_option('seo_controls');
418 if ($seo_controls['enable_googleinstantindex'] ?? false) {
419 $menu_items['instant_index'] = [
420 'title' => 'Instant Indexing',
421 'slug_suffix' => '-instant-index',
422 'callback' => 'create_admin_google_instant_index_page',
423 'internal_nav' => 'Instant Indexing',
424 'group' => 'seo'
425 ];
426 }
427
428 if ($general_options['enable_google_console'] ?? false) {
429 $menu_items['google_console'] = [
430 'title' => 'Google Console',
431 'slug_suffix' => '-google-console',
432 'callback' => 'create_admin_google_console_page',
433 'internal_nav' => 'Google Console',
434 'group' => 'seo'
435 ];
436 }
437
438 if ($general_options['enable_bing_console'] ?? false) {
439 $menu_items['bing_console'] = [
440 'title' => 'Bing Console',
441 'slug_suffix' => '-bing-console',
442 'callback' => 'create_admin_bing_console_page',
443 'internal_nav' => 'Bing Console',
444 'group' => 'seo'
445 ];
446 }
447
448 // Redirections page (check access control)
449 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
450 $menu_items['redirections'] = [
451 'title' => 'Redirections',
452 'slug_suffix' => '-redirections',
453 'callback' => 'create_admin_redirections_page',
454 'internal_nav' => 'Redirections',
455 'group' => 'seo'
456 ];
457 }
458
459 // Robots.txt page (check access control)
460 if (Metasync_Access_Control::user_can_access('hide_robots')) {
461 $menu_items['robots_txt'] = [
462 'title' => 'Robots.txt',
463 'slug_suffix' => '-robots-txt',
464 'callback' => 'create_admin_robots_txt_page',
465 'internal_nav' => 'Robots.txt',
466 'group' => 'seo'
467 ];
468 }
469
470 // XML Sitemap page (check access control)
471 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
472 $menu_items['xml_sitemap'] = [
473 'title' => 'XML Sitemap',
474 'slug_suffix' => '-xml-sitemap',
475 'callback' => 'create_admin_xml_sitemap_page',
476 'internal_nav' => 'XML Sitemap',
477 'group' => 'seo'
478 ];
479 }
480
481 // Schema Markup
482 $menu_items['schema_markup'] = [
483 'title' => 'Schema Markup',
484 'slug_suffix' => '-schema-markup',
485 'callback' => 'create_admin_schema_markup_page',
486 'internal_nav' => 'Schema Markup',
487 'group' => 'seo'
488 ];
489
490 // 404 Monitor (always available — SEO monitoring tool)
491 $menu_items['monitor_404'] = [
492 'title' => '404 Monitor',
493 'slug_suffix' => '-404-monitor',
494 'callback' => 'create_admin_404_monitor_page',
495 'internal_nav' => '404 Monitor',
496 'group' => 'seo'
497 ];
498
499 // Site Verification
500 $menu_items['site_verification'] = [
501 'title' => 'Site Verification',
502 'slug_suffix' => '-search-engine-verify',
503 'callback' => 'create_admin_search_engine_verification_page',
504 'internal_nav' => 'Site Verification',
505 'group' => 'seo'
506 ];
507
508 // SEO Health dashboard (always available)
509 $menu_items['seo_health'] = [
510 'title' => 'SEO Health',
511 'slug_suffix' => '-seo-health',
512 'callback' => 'create_admin_seo_health_page',
513 'internal_nav' => 'SEO Health',
514 'group' => 'seo'
515 ];
516
517 // Import SEO Data page (check access control)
518 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
519 $menu_items['import_seo'] = [
520 'title' => 'Import SEO Data',
521 'slug_suffix' => '-import-external',
522 'callback' => 'render_import_external_data_page',
523 'internal_nav' => 'Import SEO Data',
524 'group' => 'seo'
525 ];
526 }
527
528 // === PLUGIN GROUP ===
529
530 // Settings (check access control)
531 if (Metasync_Access_Control::user_can_access('hide_settings')) {
532 $menu_items['general'] = [
533 'title' => 'Settings',
534 'slug_suffix' => '',
535 'callback' => 'create_admin_settings_page',
536 'internal_nav' => 'General Settings',
537 'group' => 'plugin'
538 ];
539 }
540
541 // Local Business
542 $menu_items['local_business'] = [
543 'title' => 'Local Business',
544 'slug_suffix' => '-local-business',
545 'callback' => 'create_admin_local_business_page',
546 'internal_nav' => 'Local Business',
547 'group' => 'seo'
548 ];
549
550 // Breadcrumbs
551 $menu_items['breadcrumbs'] = [
552 'title' => 'Breadcrumbs',
553 'slug_suffix' => '-breadcrumbs',
554 'callback' => 'create_admin_breadcrumbs_page',
555 'internal_nav' => 'Breadcrumbs',
556 'group' => 'seo'
557 ];
558
559 // Code Snippets
560 $menu_items['code_snippets'] = [
561 'title' => 'Code Snippets',
562 'slug_suffix' => '-code-snippets',
563 'callback' => 'create_admin_code_snippets_page',
564 'internal_nav' => 'Code Snippets',
565 'group' => 'plugin'
566 ];
567
568 // Code Minification
569 $menu_items['code_minification'] = [
570 'title' => 'Code Minification',
571 'slug_suffix' => '-code-minification',
572 'callback' => 'create_admin_code_minification_page',
573 'internal_nav' => 'Code Minification',
574 'group' => 'plugin'
575 ];
576
577 // Media Optimization
578 $menu_items['media_optimization'] = [
579 'title' => 'Media Optimization',
580 'slug_suffix' => '-media-optimization',
581 'callback' => 'create_admin_media_optimization_page',
582 'internal_nav' => 'Media Optimization',
583 'group' => 'plugin'
584 ];
585
586 // Custom HTML Pages (check access control)
587 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
588 $menu_items['custom_pages'] = [
589 'title' => 'Custom Pages',
590 'slug_suffix' => '-custom-pages',
591 'callback' => 'create_admin_custom_pages_page',
592 'internal_nav' => 'Custom HTML Pages',
593 'group' => 'plugin'
594 ];
595 }
596
597 if ($general_options['enable_optimal_settings'] ?? false) {
598 $menu_items['optimal_settings'] = [
599 'title' => 'Optimal Settings',
600 'slug_suffix' => '-optimal-settings',
601 'callback' => 'create_admin_optimal_settings_page',
602 'internal_nav' => 'Optimal Settings',
603 'group' => 'plugin'
604 ];
605 }
606
607 // Compatibility page (check access control)
608 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
609 $menu_items['compatibility'] = [
610 'title' => 'Compatibility',
611 'slug_suffix' => '-compatibility',
612 'callback' => 'create_admin_compatibility_page',
613 'internal_nav' => 'Compatibility',
614 'group' => 'plugin'
615 ];
616 }
617
618 // Sync Log page (check access control)
619 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
620 $menu_items['sync_log'] = [
621 'title' => 'Changes Log',
622 'slug_suffix' => '-sync-log',
623 'callback' => 'create_admin_sync_log_page',
624 'internal_nav' => 'Changes Log',
625 'group' => 'plugin'
626 ];
627 }
628
629 // Bot Statistics
630 $menu_items['bot_statistics'] = [
631 'title' => 'Bot Statistics',
632 'slug_suffix' => '-bot-statistics',
633 'callback' => 'create_admin_bot_statistics_page',
634 'internal_nav' => 'Bot Statistics',
635 'group' => 'plugin'
636 ];
637
638 // Report Issue page (check access control)
639 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
640 $menu_items['report_issue'] = [
641 'title' => 'Report Issue',
642 'slug_suffix' => '-report-issue',
643 'callback' => 'create_admin_report_issue_page',
644 'internal_nav' => 'Report Issue',
645 'group' => 'plugin'
646 ];
647 }
648
649 return $menu_items;
650 }
651
652 /**
653 * Register all WordPress admin menu / submenu pages.
654 *
655 * @param Metasync_Admin $admin The admin instance whose callbacks WordPress will invoke.
656 */
657 public function add_plugin_settings_page($admin)
658 {
659 if (!Metasync::current_user_has_plugin_access()) {
660 return;
661 }
662
663 $data = Metasync::get_option('general');
664 $plugin_name = Metasync::get_effective_plugin_name();
665 $menu_name = $plugin_name;
666 $menu_title = $plugin_name;
667 // Sanitize so a URL-shaped legacy value still yields a valid WP menu slug
668 $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']);
669 $menu_slug = $menu_slug === '' ? Metasync_Admin::$page_slug : $menu_slug;
670 $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'];
671
672 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
673 $menu_capability = 'read';
674
675 // Separator just before the plugin entry, after all other plugins (Yoast etc. ~99)
676 add_action('admin_menu', function() {
677 global $menu;
678 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
679 }, 5);
680
681 // Main menu page at position 100.1 — bottom of the menu after all standard items
682 add_menu_page(
683 $menu_name,
684 $menu_title,
685 $menu_capability,
686 $menu_slug,
687 array($admin, 'create_admin_settings_page'),
688 $menu_icon,
689 '100.1'
690 );
691
692 // Check connection status for submenu availability
693 $general_options = Metasync::get_option('general');
694 $has_api_key = !empty($general_options['searchatlas_api_key']);
695 $has_uuid = !empty($general_options['otto_pixel_uuid']);
696 $is_fully_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_options);
697
698 $seo_controls = Metasync::get_option('seo_controls');
699
700 // ── Connect slug ──────────────────────────────────────────────────
701 $connect_slug = $menu_slug . '-connect';
702
703 // Dashboard (check access control + "Hide Dashboard" general setting)
704 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
705 add_submenu_page($menu_slug, 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
706 }
707
708 // Indexation Control
709 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
710 add_submenu_page($menu_slug, 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page'));
711 }
712
713 // 404 Monitor
714 add_submenu_page($menu_slug, '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page'));
715
716 // Redirections
717 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
718 add_submenu_page($menu_slug, 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page'));
719 }
720
721 // XML Sitemap
722 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
723 add_submenu_page($menu_slug, 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page'));
724 }
725
726 // Robots.txt
727 if (Metasync_Access_Control::user_can_access('hide_robots')) {
728 add_submenu_page($menu_slug, 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page'));
729 }
730
731 // Site Verification
732 add_submenu_page($menu_slug, 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
733
734 // Instant Indexing (conditional)
735 if ($seo_controls['enable_googleinstantindex'] ?? false) {
736 add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
737 }
738
739 // Google Console (conditional)
740 if ($general_options['enable_google_console'] ?? false) {
741 add_submenu_page($menu_slug, 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
742 }
743
744 // Bing Console (conditional)
745 if ($seo_controls['enable_binginstantindex'] ?? false) {
746 add_submenu_page($menu_slug, 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page'));
747 }
748
749 // Schema Markup
750 add_submenu_page($menu_slug, 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page'));
751
752 // Import SEO Data
753 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
754 add_submenu_page($menu_slug, 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page'));
755 }
756
757 // Settings
758 if (Metasync_Access_Control::user_can_access('hide_settings')) {
759 add_submenu_page($menu_slug, 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page'));
760 }
761
762 // Optimal Settings (conditional)
763 if ($general_options['enable_optimal_settings'] ?? false) {
764 add_submenu_page($menu_slug, 'Optimal Settings', 'Optimal Settings', $menu_capability, $menu_slug . '-optimal-settings', array($admin, 'create_admin_optimal_settings_page'));
765 }
766
767 // Local Business
768 add_submenu_page($menu_slug, 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page'));
769
770 // Breadcrumbs
771 add_submenu_page($menu_slug, 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page'));
772
773 // Code Snippets
774 add_submenu_page($menu_slug, 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page'));
775
776 // Code Minification
777 add_submenu_page($menu_slug, 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
778
779 // Media Optimization
780 add_submenu_page($menu_slug, 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page'));
781
782 // Custom Pages
783 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
784 add_submenu_page($menu_slug, 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page'));
785 }
786
787 // Bot Statistics
788 add_submenu_page($menu_slug, 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page'));
789
790 // SEO Health dashboard
791 add_submenu_page($menu_slug, 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page'));
792
793 // Compatibility
794 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
795 add_submenu_page($menu_slug, 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page'));
796 }
797
798 // Changes Log
799 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
800 add_submenu_page($menu_slug, 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page'));
801 }
802
803 // Report Issue
804 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
805 add_submenu_page($menu_slug, 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page'));
806 }
807
808 // ── Connect CTA (shown when not authenticated) ────────────────────
809 if (!$is_fully_connected) {
810 $connect_label = sprintf('Connect to %s', Metasync::get_effective_plugin_name());
811 add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
812 }
813
814 // ── Hidden pages (no sidebar entry needed) ────────────────────────
815 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
816
817
818 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
819 add_action('admin_menu', function() use ($menu_slug) {
820 global $submenu;
821 if (!isset($submenu[$menu_slug])) {
822 return;
823 }
824 // Remove the auto-duplicate of the main menu item (same slug as parent)
825 foreach ($submenu[$menu_slug] as $key => $item) {
826 if ($item[2] === $menu_slug) {
827 unset($submenu[$menu_slug][$key]);
828 break;
829 }
830 }
831 }, 999);
832
833 }
834
835 /**
836 * Render the grouped navigation menu with Dashboard + SEO/Plugin dropdowns.
837 */
838 public function render_navigation_menu($current_page = null)
839 {
840 $page_slug = Metasync_Admin::$page_slug;
841 $available_menu_items = $this->get_available_menu_items();
842
843 $menu_icons = [
844 'general' => 'admin-settings',
845 'dashboard' => 'dashboard',
846 'compatibility' => 'admin-tools',
847 'sync_log' => 'list-view',
848 'seo_controls' => 'search',
849 'optimal_settings' => 'superhero-alt',
850 'instant_index' => 'performance',
851 'google_console' => 'chart-area',
852 'bing_console' => 'chart-bar',
853 'redirections' => 'undo',
854 'robots_txt' => 'shield',
855 'xml_sitemap' => 'networking',
856 'schema_markup' => 'tag',
857 'site_verification'=> 'yes-alt',
858 'import_seo' => 'download',
859 'custom_pages' => 'admin-page',
860 'code_minification'=> 'media-code',
861 'bot_statistics' => 'visibility',
862 'breadcrumbs' => 'menu',
863 'monitor_404' => 'warning',
864 'local_business' => 'building',
865 'code_snippets' => 'editor-code',
866 'report_issue' => 'sos',
867 'error_log' => 'warning',
868 'seo_health' => 'heart'
869
870 ];
871
872 $seo_items = [];
873 $plugin_items = [];
874
875 foreach ($available_menu_items as $key => $menu_item) {
876 $group = $menu_item['group'] ?? 'plugin';
877 if ($group === 'seo') {
878 $seo_items[$key] = $menu_item;
879 } else {
880 $plugin_items[$key] = $menu_item;
881 }
882 }
883 ?>
884 <!-- Plugin Navigation Menu with Dashboard + Grouped Dropdowns -->
885 <div class="metasync-nav-wrapper">
886 <div class="metasync-nav-tabs metasync-nav-grouped">
887 <?php
888 // Dashboard tab (standalone)
889 if (isset($seo_items['dashboard'])) {
890 $is_active = ($current_page === 'dashboard');
891 $icon = $menu_icons['dashboard'] ?? 'admin-generic';
892 $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix'];
893 ?>
894 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
895 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
896 <span class="tab-text"><?php echo esc_html($seo_items['dashboard']['title']); ?></span>
897 </a>
898 <?php
899 }
900 ?>
901
902 <!-- SEO Features Dropdown (excluding Dashboard) -->
903 <div class="metasync-nav-dropdown">
904 <?php
905 $has_active_seo = false;
906 foreach ($seo_items as $key => $menu_item) {
907 if ($key !== 'dashboard' && $current_page === $key) {
908 $has_active_seo = true;
909 break;
910 }
911 }
912 ?>
913 <button type="button" class="metasync-nav-dropdown-btn <?php echo $has_active_seo ? 'active' : ''; ?>" aria-haspopup="true" aria-expanded="false">
914 <span class="tab-icon"><span class="dashicons dashicons-search"></span></span>
915 <span class="tab-text">SEO</span>
916 <span class="dropdown-arrow"></span>
917 </button>
918 <div class="metasync-nav-dropdown-menu">
919 <?php
920 foreach ($seo_items as $key => $menu_item) {
921 if ($key === 'dashboard') {
922 continue;
923 }
924
925 $is_active = ($current_page === $key);
926 $icon = $menu_icons[$key] ?? 'admin-generic';
927 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
928 ?>
929 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-dropdown-item <?php echo $is_active ? 'active' : ''; ?>">
930 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
931 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
932 </a>
933 <?php
934 }
935 ?>
936 </div>
937 </div>
938
939 <!-- Plugin Dropdown -->
940 <div class="metasync-nav-dropdown">
941 <?php
942 $has_active_plugin = false;
943 foreach ($plugin_items as $key => $menu_item) {
944 if ($key !== 'report_issue' && $current_page === $key) {
945 $has_active_plugin = true;
946 break;
947 }
948 }
949 ?>
950 <button type="button" class="metasync-nav-dropdown-btn <?php echo $has_active_plugin ? 'active' : ''; ?>" aria-haspopup="true" aria-expanded="false">
951 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
952 <span class="tab-text">Plugin</span>
953 <span class="dropdown-arrow"></span>
954 </button>
955 <div class="metasync-nav-dropdown-menu">
956 <?php
957 foreach ($plugin_items as $key => $menu_item) {
958 if ($key === 'report_issue') {
959 continue;
960 }
961
962 $is_active = ($current_page === $key);
963 $icon = $menu_icons[$key] ?? 'admin-generic';
964 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
965 ?>
966 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-dropdown-item <?php echo $is_active ? 'active' : ''; ?>">
967 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
968 <span class="tab-text"><?php echo esc_html($menu_item['title']); ?></span>
969 </a>
970 <?php
971 }
972 ?>
973 </div>
974 </div>
975
976 <!-- Right side - Report Issue and Settings dropdown -->
977 <div class="metasync-nav-right">
978 <?php
979 if (isset($available_menu_items['report_issue'])) {
980 $is_active = ($current_page === 'report_issue');
981 $page_url = '?page=' . $page_slug . $available_menu_items['report_issue']['slug_suffix'];
982 ?>
983 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
984 <span class="tab-icon"><span class="dashicons dashicons-sos"></span></span>
985 <span class="tab-text">Report Issue</span>
986 </a>
987 <?php } ?>
988 <div class="metasync-simple-dropdown">
989 <button type="button" class="metasync-settings-btn" id="metasync-settings-btn" onclick="toggleSettingsMenuPortal(event)" aria-expanded="false">
990 <span class="tab-icon"><span class="dashicons dashicons-admin-settings"></span></span>
991 <span class="tab-text">Settings</span>
992 <span class="dropdown-arrow"></span>
993 </button>
994 </div>
995 </div>
996 </div>
997 </div>
998
999 <script>
1000 // Handle navigation dropdowns using portal pattern for reliable positioning
1001 (function() {
1002 var dropdowns = document.querySelectorAll('.metasync-nav-dropdown');
1003 var activePortal = null;
1004 var activeButton = null;
1005 var activeDropdown = null;
1006 var scrollHandler = null;
1007 var resizeHandler = null;
1008
1009 function positionPortalMenu(button, portalMenu) {
1010 var rect = button.getBoundingClientRect();
1011 var menuRect = portalMenu.getBoundingClientRect();
1012 var viewportWidth = window.innerWidth;
1013 var viewportHeight = window.innerHeight;
1014
1015 var left = rect.left;
1016 if (left + menuRect.width > viewportWidth - 10) {
1017 left = Math.max(10, viewportWidth - menuRect.width - 10);
1018 }
1019
1020 var top = rect.bottom + 8;
1021 if (top + menuRect.height > viewportHeight - 10 && rect.top > menuRect.height + 10) {
1022 top = rect.top - menuRect.height - 8;
1023 }
1024
1025 portalMenu.style.position = 'fixed';
1026 portalMenu.style.top = top + 'px';
1027 portalMenu.style.left = left + 'px';
1028 portalMenu.style.zIndex = '999999999';
1029 }
1030
1031 function closeActivePortal() {
1032 if (activePortal && activePortal.parentNode) {
1033 activePortal.parentNode.removeChild(activePortal);
1034 }
1035 if (activeButton) {
1036 activeButton.setAttribute('aria-expanded', 'false');
1037 }
1038 if (activeDropdown) {
1039 activeDropdown.classList.remove('active');
1040 }
1041 if (scrollHandler) {
1042 window.removeEventListener('scroll', scrollHandler, true);
1043 scrollHandler = null;
1044 }
1045 if (resizeHandler) {
1046 window.removeEventListener('resize', resizeHandler);
1047 resizeHandler = null;
1048 }
1049 activePortal = null;
1050 activeButton = null;
1051 activeDropdown = null;
1052 }
1053
1054 function openAsPortal(dropdown, button, menu) {
1055 closeActivePortal();
1056
1057 var portalMenu = menu.cloneNode(true);
1058 portalMenu.id = 'metasync-nav-portal-menu';
1059 portalMenu.style.opacity = '1';
1060 portalMenu.style.visibility = 'visible';
1061 portalMenu.style.transform = 'none';
1062
1063 document.body.appendChild(portalMenu);
1064
1065 positionPortalMenu(button, portalMenu);
1066
1067 activePortal = portalMenu;
1068 activeButton = button;
1069 activeDropdown = dropdown;
1070 dropdown.classList.add('active');
1071 button.setAttribute('aria-expanded', 'true');
1072
1073 scrollHandler = function() {
1074 if (activePortal && activeButton) {
1075 positionPortalMenu(activeButton, activePortal);
1076 }
1077 };
1078 resizeHandler = scrollHandler;
1079
1080 window.addEventListener('scroll', scrollHandler, true);
1081 window.addEventListener('resize', resizeHandler);
1082
1083 portalMenu.addEventListener('click', function(e) {
1084 var link = e.target.closest('a');
1085 if (link) {
1086 setTimeout(closeActivePortal, 0);
1087 }
1088 });
1089 }
1090
1091 dropdowns.forEach(function(dropdown) {
1092 var button = dropdown.querySelector('.metasync-nav-dropdown-btn');
1093 var menu = dropdown.querySelector('.metasync-nav-dropdown-menu');
1094
1095 if (!button || !menu) return;
1096
1097 button.addEventListener('click', function(e) {
1098 e.preventDefault();
1099 e.stopPropagation();
1100
1101 var isExpanded = button.getAttribute('aria-expanded') === 'true';
1102
1103 if (isExpanded) {
1104 closeActivePortal();
1105 } else {
1106 openAsPortal(dropdown, button, menu);
1107 }
1108 });
1109 });
1110
1111 document.addEventListener('click', function(e) {
1112 if (activePortal && activeButton) {
1113 if (!activePortal.contains(e.target) && !activeButton.contains(e.target)) {
1114 closeActivePortal();
1115 }
1116 }
1117 });
1118
1119 document.addEventListener('keydown', function(e) {
1120 if (e.key === 'Escape' && activePortal) {
1121 closeActivePortal();
1122 if (activeButton) {
1123 activeButton.focus();
1124 }
1125 }
1126 });
1127 })();
1128
1129 // Portal-style dropdown that bypasses stacking contexts
1130 function toggleSettingsMenuPortal(event) {
1131 event.preventDefault();
1132 event.stopPropagation();
1133
1134 var button = event.currentTarget;
1135 var existingMenu = document.getElementById('metasync-portal-menu');
1136
1137 if (existingMenu) {
1138 existingMenu.remove();
1139 button.classList.remove('active');
1140 button.setAttribute('aria-expanded', 'false');
1141 return;
1142 }
1143
1144 var menu = document.createElement('div');
1145 menu.id = 'metasync-portal-menu';
1146 menu.className = 'metasync-portal-menu';
1147
1148 var currentUrl = window.location.href;
1149 var isGeneralActive = currentUrl.indexOf('tab=general') > -1 || currentUrl.indexOf('tab=') === -1;
1150 var isAdvancedActive = currentUrl.indexOf('tab=advanced') > -1;
1151 var isWhitelabelActive = currentUrl.indexOf('tab=whitelabel') > -1;
1152
1153 menu.textContent = '';
1154
1155 var hideAdvanced = <?php
1156 echo !Metasync_Access_Control::user_can_access('hide_advanced') ? 'true' : 'false';
1157 ?>;
1158 var showGeneral = <?php
1159 echo Metasync_Access_Control::user_can_access('hide_settings') ? 'true' : 'false';
1160 ?>;
1161
1162 if (showGeneral) {
1163 const generalLink = document.createElement('a');
1164 generalLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=general';
1165 generalLink.className = 'metasync-portal-item' + (isGeneralActive ? ' active' : '');
1166 generalLink.textContent = 'General';
1167 menu.appendChild(generalLink);
1168 }
1169
1170 const whitelabelLink = document.createElement('a');
1171 whitelabelLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=whitelabel';
1172 whitelabelLink.className = 'metasync-portal-item' + (isWhitelabelActive ? ' active' : '');
1173 whitelabelLink.textContent = 'White label';
1174 menu.appendChild(whitelabelLink);
1175
1176 if (!hideAdvanced) {
1177 const advancedLink = document.createElement('a');
1178 advancedLink.href = '?page=<?php echo esc_js( $page_slug ); ?>&tab=advanced';
1179 advancedLink.className = 'metasync-portal-item' + (isAdvancedActive ? ' active' : '');
1180 advancedLink.textContent = 'Advanced';
1181 menu.appendChild(advancedLink);
1182 }
1183
1184
1185 var rect = button.getBoundingClientRect();
1186 menu.style.position = 'fixed';
1187 menu.style.top = (rect.bottom + 8) + 'px';
1188 menu.style.right = (window.innerWidth - rect.right) + 'px';
1189 menu.style.zIndex = '999999999';
1190
1191 document.body.appendChild(menu);
1192
1193 button.classList.add('active');
1194 button.setAttribute('aria-expanded', 'true');
1195 }
1196
1197 document.addEventListener('click', function(event) {
1198 var button = document.getElementById('metasync-settings-btn');
1199 var menu = document.getElementById('metasync-portal-menu');
1200
1201 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
1202 menu.remove();
1203 button.classList.remove('active');
1204 button.setAttribute('aria-expanded', 'false');
1205 }
1206 });
1207 </script>
1208 <?php
1209 }
1210
1211 /**
1212 * Render the plugin header with logo, theme toggle, and connection badge.
1213 */
1214 public function render_plugin_header($page_title = null)
1215 {
1216 $general_settings = Metasync::get_option('general');
1217
1218 $effective_plugin_name = Metasync::get_effective_plugin_name();
1219 $display_title = $page_title ?: $effective_plugin_name;
1220 $logo = $this->resolve_logo_data();
1221
1222 $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
1223 $otto_pixel_uuid = isset($general_settings['otto_pixel_uuid']) ? $general_settings['otto_pixel_uuid'] : '';
1224
1225 $is_integrated = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1226
1227 $current_theme = get_option('metasync_theme', 'dark');
1228 ?>
1229
1230 <!-- Plugin Header with Logo -->
1231 <div class="metasync-header" data-current-theme="<?php echo esc_attr($current_theme); ?>">
1232 <div class="metasync-header-left">
1233 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1234 <div class="metasync-logo-container">
1235 <img src="<?php echo esc_url($logo['light_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-light" />
1236 <img src="<?php echo esc_url($logo['dark_url']); ?>" alt="Logo" class="metasync-logo metasync-logo-dark" />
1237 </div>
1238 <?php elseif ($logo['show_logo'] && !empty($logo['url'])): ?>
1239 <div class="metasync-logo-container">
1240 <img src="<?php echo esc_url($logo['url']); ?>" alt="Logo" class="metasync-logo<?php echo !empty($logo['is_default']) ? ' metasync-logo-default' : ''; ?>" />
1241 </div>
1242 <?php endif; ?>
1243 </div>
1244
1245 <div class="metasync-header-right">
1246 <!-- Theme Toggle -->
1247 <div class="metasync-theme-toggle" role="group" aria-label="Theme Selector">
1248 <button class="metasync-theme-option <?php echo ($current_theme === 'light') ? 'active' : ''; ?>" data-theme="light" aria-label="Light Theme" type="button">
1249 <span class="metasync-theme-icon">&#9728;</span>
1250 <span class="theme-label">Light</span>
1251 </button>
1252 <button class="metasync-theme-option <?php echo ($current_theme === 'dark') ? 'active' : ''; ?>" data-theme="dark" aria-label="Dark Theme" type="button">
1253 <span class="metasync-theme-icon">&#9790;</span>
1254 <span class="theme-label">Dark</span>
1255 </button>
1256 </div>
1257
1258 <!-- Integration Status -->
1259 <?php
1260 if ($is_integrated && !empty($otto_pixel_uuid)) {
1261 $status_class_header = 'integrated';
1262 $status_title_header = 'Synced - Heartbeat API connectivity verified';
1263 $status_text_header = 'Synced';
1264 } elseif ($is_integrated && empty($otto_pixel_uuid)) {
1265 $status_class_header = 'warning';
1266 $status_title_header = 'Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1267 $status_text_header = 'Warning';
1268 } else {
1269 $status_class_header = 'not-integrated';
1270 $status_title_header = 'Not Synced - Heartbeat API not responding or unreachable';
1271 $status_text_header = 'Not Synced';
1272 }
1273 ?>
1274 <div class="metasync-integration-status <?php echo $status_class_header; ?>"
1275 title="<?php echo esc_attr($status_title_header); ?>">
1276 <span class="status-indicator"></span>
1277 <span class="status-text"><?php echo esc_html($status_text_header); ?></span>
1278 </div>
1279 </div>
1280 </div>
1281
1282 <!-- Page Title Below Header -->
1283 <div class="metasync-page-title">
1284 <h1><?php echo esc_html($display_title); ?></h1>
1285 </div>
1286
1287 <?php
1288 }
1289
1290 // ------------------------------------------------------------------
1291 // Admin bar status indicator
1292 // ------------------------------------------------------------------
1293
1294 /**
1295 * Output inline CSS (and optional JS) for the admin-bar status node.
1296 */
1297 public function metasync_admin_bar_style()
1298 {
1299 if (!is_admin_bar_showing()) {
1300 return;
1301 }
1302
1303 # For backward compatibility, constant takes precedence.
1304 if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) {
1305 return;
1306 }
1307
1308
1309 # Check if admin bar status is enabled via setting
1310 $general_settings = Metasync::get_option('general');
1311 $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true;
1312 if (!$show_admin_bar) {
1313 return;
1314 }
1315 ?>
1316 <style type="text/css">
1317 #wp-admin-bar-searchatlas-status .ab-item {
1318 font-weight: 500 !important;
1319 transition: all 0.2s ease !important;
1320 }
1321
1322 #wp-admin-bar-searchatlas-status:hover .ab-item {
1323 background-color: rgba(255, 255, 255, 0.1) !important;
1324 }
1325
1326 #wp-admin-bar-searchatlas-status.searchatlas-synced .ab-item {
1327 color: #46b450 !important; /* WordPress green for text */
1328 }
1329
1330 #wp-admin-bar-searchatlas-status.searchatlas-not-synced .ab-item {
1331 color: #dc3232 !important; /* WordPress red for text */
1332 }
1333
1334 #wp-admin-bar-searchatlas-status.searchatlas-warning .ab-item {
1335 color: #ffb900 !important; /* WordPress yellow for warning */
1336 }
1337
1338 /* Ensure emojis maintain their natural colors */
1339 #wp-admin-bar-searchatlas-status .ab-item {
1340 filter: none !important;
1341 }
1342
1343 /* Make status visible but subtle */
1344 #wp-admin-bar-searchatlas-status .ab-item {
1345 opacity: 0.9;
1346 }
1347
1348 #wp-admin-bar-searchatlas-status:hover .ab-item {
1349 opacity: 1;
1350 }
1351 </style>
1352
1353 <?php
1354 $page_slug = Metasync_Admin::$page_slug;
1355 $is_plugin_page = (
1356 (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1357 (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false)
1358 );
1359 if ($is_plugin_page):
1360 ?>
1361 <script type="text/javascript">
1362 // Pass PHP variables to JavaScript
1363 window.MetasyncConfig = {
1364 pluginName: '<?php echo esc_js(Metasync::get_effective_plugin_name()); ?>',
1365 ottoName: '<?php echo esc_js(Metasync::get_whitelabel_otto_name()); ?>'
1366 };
1367
1368 jQuery(document).ready(function($) {
1369
1370 // Function to sync admin bar status
1371 function syncAdminBarStatus() {
1372 var pluginPageStatus = $('.metasync-integration-status .status-text').text();
1373 var adminBarItem = $('#wp-admin-bar-searchatlas-status .ab-item');
1374 var adminBarContainer = $('#wp-admin-bar-searchatlas-status');
1375 var pluginName = window.MetasyncConfig.pluginName;
1376
1377 if (pluginPageStatus && adminBarItem.length) {
1378 var allClasses = 'searchatlas-synced searchatlas-not-synced searchatlas-warning';
1379
1380 // Helper to update emoji in admin bar
1381 function updateAdminBarEmoji(targetEmoji, targetSvgCode) {
1382 var emojiImg = adminBarItem.find('img.emoji');
1383 if (emojiImg.length > 0) {
1384 emojiImg.attr('alt', targetEmoji);
1385 var currentSrc = emojiImg.attr('src');
1386 var updatedSrc = currentSrc.replace(/1f7e2\.svg|1f534\.svg|1f7e1\.svg/, targetSvgCode + '.svg');
1387 emojiImg.attr('src', updatedSrc);
1388 } else {
1389 var newHtml = adminBarItem.html().replace(/🟢|🔴|🟡/, targetEmoji);
1390 if (!newHtml.includes(targetEmoji) && newHtml.includes(pluginName)) {
1391 newHtml = newHtml.replace(pluginName, pluginName + ' ' + targetEmoji);
1392 }
1393 adminBarItem.html(newHtml);
1394 }
1395 }
1396
1397 if (pluginPageStatus.includes('Synced') && !pluginPageStatus.includes('Not Synced')) {
1398 // Update admin bar to synced (GREEN)
1399 updateAdminBarEmoji('🟢', '1f7e2');
1400 adminBarContainer.removeClass(allClasses).addClass('searchatlas-synced');
1401 var syncTitle = pluginName + ' - Synced (Heartbeat API connectivity verified)';
1402 adminBarContainer.attr('title', syncTitle);
1403 adminBarItem.attr('title', syncTitle);
1404
1405 } else if (pluginPageStatus.includes('Warning')) {
1406 // Update admin bar to warning (YELLOW)
1407 updateAdminBarEmoji('🟡', '1f7e1');
1408 adminBarContainer.removeClass(allClasses).addClass('searchatlas-warning');
1409 var warnTitle = pluginName + ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1410 adminBarContainer.attr('title', warnTitle);
1411 adminBarItem.attr('title', warnTitle);
1412
1413 } else if (pluginPageStatus.includes('Not Synced')) {
1414 // Update admin bar to not synced (RED)
1415 updateAdminBarEmoji('🔴', '1f534');
1416 adminBarContainer.removeClass(allClasses).addClass('searchatlas-not-synced');
1417 var notSyncTitle = pluginName + ' - Not Synced (Heartbeat API not responding or unreachable)';
1418 adminBarContainer.attr('title', notSyncTitle);
1419 adminBarItem.attr('title', notSyncTitle);
1420 }
1421 }
1422 }
1423
1424 // Sync when tabs are switched (for General/Advanced tabs)
1425 $(document).on('click', 'a[href*="tab="]', function() {
1426 setTimeout(syncAdminBarStatus, 200);
1427 });
1428
1429 // Also check every 5 seconds to keep it in sync
1430 setInterval(syncAdminBarStatus, 5000);
1431 });
1432 </script>
1433 <?php endif; ?>
1434 <?php
1435 }
1436
1437 /**
1438 * Add Search Atlas status indicator to WordPress admin bar.
1439 */
1440 public function add_searchatlas_admin_bar_status($wp_admin_bar)
1441 {
1442 if (!Metasync::current_user_has_plugin_access()) {
1443 return;
1444 }
1445
1446 # For backward compatibility, constant takes precedence.
1447 if (defined('METASYNC_SHOW_ADMIN_BAR_STATUS') && !METASYNC_SHOW_ADMIN_BAR_STATUS) {
1448 return;
1449 }
1450
1451 # Check if admin bar status is disabled via setting
1452 $general_settings = Metasync::get_option('general');
1453 if (!is_array($general_settings)) {
1454 $general_settings = [];
1455 }
1456 $show_admin_bar = $general_settings['show_admin_bar_status'] ?? true;
1457 if (!$show_admin_bar) {
1458 return;
1459 }
1460
1461 if (!is_admin() && !apply_filters('metasync_show_admin_bar_status_frontend', false)) {
1462 return;
1463 }
1464
1465 $node = get_transient(self::CACHE_KEY);
1466
1467 if ($node === false) {
1468 $is_synced = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1469
1470 $plugin_name = Metasync::get_effective_plugin_name();
1471
1472 if ($is_synced) {
1473 $otto_uuid = $general_settings['otto_pixel_uuid'] ?? '';
1474 if (!empty($otto_uuid)) {
1475 $status_emoji = '🟢'; // Green circle for synced
1476 $title = $plugin_name . ' - Synced (Heartbeat API connectivity verified)';
1477 $status_class = 'searchatlas-synced';
1478 } else {
1479 $status_emoji = '🟡'; // Yellow circle for warning
1480 $title = $plugin_name . ' - Connected but OTTO UUID is missing — deploys will not work. Please reconnect.';
1481 $status_class = 'searchatlas-warning';
1482 }
1483 } else {
1484 $status_emoji = '🔴'; // Red circle for not synced
1485 $title = $plugin_name . ' - Not Synced (Heartbeat API not responding or unreachable)';
1486 $status_class = 'searchatlas-not-synced';
1487 }
1488
1489 $admin_bar_title = $plugin_name . ' ' . $status_emoji;
1490
1491 $node = array(
1492 'title' => $admin_bar_title,
1493 'href' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug),
1494 'meta_title' => $title,
1495 'meta_class' => $status_class,
1496 );
1497
1498 set_transient(self::CACHE_KEY, $node, apply_filters('metasync_admin_bar_status_cache_ttl', 5 * MINUTE_IN_SECONDS));
1499 }
1500
1501 $wp_admin_bar->add_node(array(
1502 'id' => 'searchatlas-status',
1503 'title' => $node['title'],
1504 'href' => $node['href'],
1505 'meta' => array(
1506 'title' => $node['meta_title'],
1507 'class' => $node['meta_class'],
1508 ),
1509 ));
1510 }
1511
1512 /**
1513 * Invalidate the admin bar status cache so the next page load recomputes status.
1514 */
1515 public static function invalidate_admin_bar_status_cache()
1516 {
1517 delete_transient(self::CACHE_KEY);
1518 }
1519
1520 // ------------------------------------------------------------------
1521 // Yoast-style 3-column layout helpers
1522 // ------------------------------------------------------------------
1523
1524 /**
1525 * Open the 3-column page layout.
1526 * Call this at the start of every admin page callback instead of
1527 * render_plugin_header() + render_navigation_menu().
1528 * Close with render_layout_close().
1529 *
1530 * @param string $page_title Human-readable page title.
1531 * @param string $current_page Key matching get_available_menu_items() (e.g. 'general').
1532 * @param string $description Optional subtitle shown below the title.
1533 */
1534 public function render_layout_open($page_title = '', $current_page = '', $description = '')
1535 {
1536 $theme = esc_attr(get_option('metasync_theme', 'dark'));
1537 $plugin_name = Metasync::get_effective_plugin_name();
1538 $general = Metasync::get_option('general') ?? [];
1539 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1540 // badge reflects confirmed heartbeat health (connected / stale /
1541 // disconnected), not raw API-key presence.
1542 $badge = Metasync_Heartbeat_Manager::instance()->get_connection_badge($general);
1543 $logo = $this->resolve_logo_data();
1544
1545 $current_theme = get_option('metasync_theme', 'dark');
1546 ?>
1547 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo $theme; ?>">
1548
1549 <?php
1550 // Inject whitelabel color palette overrides
1551 $wl_settings = Metasync::get_whitelabel_settings();
1552 $wl_palette = isset($wl_settings['color_palette']) && is_array($wl_settings['color_palette']) ? $wl_settings['color_palette'] : array();
1553 if (!empty($wl_palette)) {
1554 echo '<style id="metasync-whitelabel-palette">';
1555 foreach (array('dark', 'light') as $t) {
1556 if (!empty($wl_palette[$t]) && is_array($wl_palette[$t])) {
1557 $selector = ($t === 'dark') ? ':root, [data-theme="dark"]' : '[data-theme="light"]';
1558 $vars = '';
1559 foreach ($wl_palette[$t] as $var_name => $color) {
1560 // Skip gradient partial keys — handled below
1561 if (strpos($var_name, 'gradient-') !== false) continue;
1562 if (preg_match('/^dashboard-[a-z-]+$/', $var_name) && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color)) {
1563 $vars .= '--' . esc_attr($var_name) . ':' . esc_attr($color) . ';';
1564 }
1565 }
1566 // Build gradient overrides from paired from/to colors
1567 $gradient_pairs = array(
1568 'dashboard-gradient-primary' => array('dashboard-gradient-primary-from', 'dashboard-gradient-primary-to'),
1569 'dashboard-gradient-accent' => array('dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'),
1570 );
1571 foreach ($gradient_pairs as $grad_var => $pair) {
1572 $from = isset($wl_palette[$t][$pair[0]]) ? $wl_palette[$t][$pair[0]] : '';
1573 $to = isset($wl_palette[$t][$pair[1]]) ? $wl_palette[$t][$pair[1]] : '';
1574 if ($from && $to && preg_match('/^#[0-9a-fA-F]{3,8}$/', $from) && preg_match('/^#[0-9a-fA-F]{3,8}$/', $to)) {
1575 $vars .= '--' . esc_attr($grad_var) . ':linear-gradient(135deg,' . esc_attr($from) . ' 0%,' . esc_attr($to) . ' 100%);';
1576 }
1577 }
1578 if ($vars) {
1579 echo $selector . '{' . $vars . '}';
1580 }
1581 }
1582 }
1583 echo '</style>';
1584 }
1585 ?>
1586
1587 <!-- Compact top header -->
1588 <div class="metasync-header-compact">
1589 <div style="display:flex;align-items:center;gap:10px;">
1590 <?php if ($logo['show_logo'] && $logo['use_dual']): ?>
1591 <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;">
1592 <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;">
1593 <?php elseif ($logo['show_logo'] && $logo['url']): ?>
1594 <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;">
1595 <?php else: ?>
1596 <strong style="font-size:15px;color:var(--dashboard-text-primary);"><?php echo esc_html($plugin_name); ?></strong>
1597 <?php endif; ?>
1598 </div>
1599 <div class="metasync-header-compact-right">
1600 <div class="metasync-status <?php echo esc_attr($badge['class']); ?>">
1601 <span class="status-dot"></span>
1602 <span class="status-text"><?php echo esc_html($badge['text']); ?></span>
1603 </div>
1604 <button type="button" class="metasync-theme-toggle" onclick="toggleMetasyncTheme()" title="Toggle theme">
1605 <span class="theme-icon-light">&#9728;</span>
1606 <span class="theme-icon-dark">&#9790;</span>
1607 </button>
1608 </div>
1609 </div>
1610
1611 <!-- 3-column layout -->
1612 <div class="metasync-layout">
1613
1614 <!-- Left sidenav -->
1615 <aside class="metasync-layout-nav">
1616 <?php $this->render_sidenav($current_page); ?>
1617 </aside>
1618
1619 <!-- Main content -->
1620 <main class="metasync-layout-main">
1621 <?php
1622 // Anchor for WP core's notice relocation (common.js). Without it, core
1623 // moves admin notices after the first .wrap h1 only on DOM-ready, after
1624 // the server has already painted them at the top of #wpbody-content —
1625 // causing a flash above the header and layout shift. Providing
1626 // an explicit .wp-header-end inside this (FOUC-hidden) wrap makes core
1627 // relocate notices here deterministically, so they fade in with the page.
1628 ?>
1629 <hr class="wp-header-end">
1630 <?php if ($page_title || $description): ?>
1631 <div class="metasync-page-header">
1632 <?php if ($page_title): ?>
1633 <h1><?php echo esc_html($page_title); ?></h1>
1634 <?php endif; ?>
1635 <?php if ($description): ?>
1636 <p><?php echo esc_html($description); ?></p>
1637 <?php endif; ?>
1638 </div>
1639 <?php endif; ?>
1640 <?php
1641 // Note: render_layout_close() closes </main>, renders promo sidebar, closes </div>.metasync-layout and </div>.wrap
1642 }
1643
1644 /**
1645 * Close the 3-column layout opened by render_layout_open().
1646 *
1647 * @param bool $show_promo Whether to render the right promo sidebar. Default true.
1648 * Pass false for full-width pages (e.g. dashboard iframe).
1649 */
1650 public function render_layout_close($show_promo = true)
1651 {
1652 ?>
1653 </main><!-- /.metasync-layout-main -->
1654
1655 <?php if ($show_promo):
1656 $general = Metasync::get_option('general') ?? [];
1657 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1658 ?>
1659 <!-- Right promo sidebar -->
1660 <aside class="metasync-layout-promo">
1661 <?php $this->render_promo_sidebar($is_connected); ?>
1662 </aside>
1663 <?php endif; ?>
1664
1665 </div><!-- /.metasync-layout -->
1666 </div><!-- /.metasync-dashboard-wrap -->
1667 <?php
1668 }
1669
1670 /**
1671 * Render the left sticky sidenav with grouped items.
1672 *
1673 * @param string $current_page Active page key.
1674 */
1675 public function render_sidenav($current_page = '')
1676 {
1677 $page_slug = Metasync_Admin::$page_slug;
1678 $menu_items = $this->get_available_menu_items();
1679
1680 // Dashicons names (without 'dashicons-' prefix) — monochrome, color set by CSS
1681 $icons = [
1682 'dashboard' => 'dashboard',
1683 'seo_controls' => 'search',
1684 'monitor_404' => 'warning',
1685 'redirections' => 'undo',
1686 'xml_sitemap' => 'networking',
1687 'robots_txt' => 'shield',
1688 'site_verification'=> 'yes-alt',
1689 'instant_index' => 'performance',
1690 'google_console' => 'chart-area',
1691 'bing_console' => 'chart-bar',
1692 'import_seo' => 'download',
1693 'general' => 'admin-settings',
1694 'schema_markup' => 'tag',
1695 'local_business' => 'building',
1696 'breadcrumbs' => 'menu',
1697 'code_snippets' => 'editor-code',
1698 'code_minification'=> 'media-code',
1699 'custom_pages' => 'admin-page',
1700 'optimal_settings' => 'superhero-alt',
1701 'compatibility' => 'admin-tools',
1702 'sync_log' => 'list-view',
1703 'bot_statistics' => 'visibility',
1704 'report_issue' => 'sos',
1705 'media_optimization'=> 'images-alt2',
1706 'seo_health' => 'heart'
1707 ];
1708
1709 $seo_items = [];
1710 $plugin_items = [];
1711 foreach ($menu_items as $key => $item) {
1712 if (($item['group'] ?? 'plugin') === 'seo') {
1713 $seo_items[$key] = $item;
1714 } else {
1715 $plugin_items[$key] = $item;
1716 }
1717 }
1718 ?>
1719 <nav class="metasync-sidenav">
1720
1721 <!-- SEO Features group -->
1722 <div class="metasync-sidenav-group">
1723 <div class="metasync-sidenav-group-title">SEO Features</div>
1724 <ul>
1725 <?php foreach ($seo_items as $key => $item):
1726 $is_active = ($current_page === $key);
1727 $icon = $icons[$key] ?? 'admin-generic';
1728 $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1729 ?>
1730 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1731 <a href="<?php echo $url; ?>">
1732 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1733 <?php echo esc_html($item['title']); ?>
1734 </a>
1735 </li>
1736 <?php endforeach; ?>
1737 </ul>
1738 </div>
1739
1740 <!-- Plugin group -->
1741 <div class="metasync-sidenav-group">
1742 <div class="metasync-sidenav-group-title">Plugin</div>
1743 <ul>
1744 <?php foreach ($plugin_items as $key => $item):
1745 if ($key === 'report_issue') continue;
1746 $is_active = ($current_page === $key);
1747 $icon = $icons[$key] ?? 'admin-generic';
1748 $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1749 ?>
1750 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1751 <a href="<?php echo $url; ?>">
1752 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1753 <?php echo esc_html($item['title']); ?>
1754 </a>
1755 </li>
1756 <?php if ($key === 'general'): ?>
1757 <li class="<?php echo $current_page === 'advanced_settings' ? 'metasync-sidenav-active' : ''; ?>" style="padding-left: 8px;">
1758 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . '&tab=advanced')); ?>">
1759 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-admin-generic"></span></span>
1760 Advanced Settings
1761 </a>
1762 </li>
1763 <li class="<?php echo $current_page === 'whitelabel' ? 'metasync-sidenav-active' : ''; ?>" style="padding-left: 8px;">
1764 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . '&tab=whitelabel')); ?>">
1765 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-tag"></span></span>
1766 Whitelabel
1767 </a>
1768 </li>
1769 <?php endif; ?>
1770 <?php endforeach; ?>
1771 </ul>
1772 </div>
1773
1774 <!-- Report Issue at the bottom -->
1775 <?php if (isset($menu_items['report_issue'])): ?>
1776 <div class="metasync-sidenav-group">
1777 <ul>
1778 <li class="<?php echo $current_page === 'report_issue' ? 'metasync-sidenav-active' : ''; ?>">
1779 <a href="<?php echo esc_url(admin_url('admin.php?page=' . $page_slug . $menu_items['report_issue']['slug_suffix'])); ?>">
1780 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-sos"></span></span>
1781 Report Issue
1782 </a>
1783 </li>
1784 </ul>
1785 </div>
1786 <?php endif; ?>
1787
1788 </nav>
1789 <?php
1790 }
1791
1792 /**
1793 * Render the right promotional sidebar.
1794 *
1795 * @param bool $is_connected Whether the plugin is authenticated.
1796 */
1797 public function render_promo_sidebar($is_connected = false)
1798 {
1799 $plugin_name = Metasync::get_effective_plugin_name();
1800 $otto_name = Metasync::get_whitelabel_otto_name();
1801 $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug));
1802 $homepage = Metasync::HOMEPAGE_DOMAIN;
1803 $is_default_brand = ( $plugin_name === 'Search Atlas' );
1804 $whitelabel = Metasync::get_whitelabel_settings();
1805 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1806 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1807 : [];
1808 ?>
1809
1810 <?php if (!$is_connected): ?>
1811 <!-- Connect CTA card -->
1812 <div class="metasync-promo-card metasync-promo-card--connect">
1813 <div class="metasync-promo-card-header">
1814 <div class="metasync-promo-card-icon">
1815 <span class="dashicons dashicons-admin-links"></span>
1816 </div>
1817 <div>
1818 <h3>Connect <?php echo esc_html($plugin_name); ?></h3>
1819 </div>
1820 </div>
1821 <p class="metasync-promo-tagline">Link your site to <?php echo esc_html($plugin_name); ?> to unlock <?php echo esc_html($otto_name); ?>, keyword data, and automated SEO.</p>
1822 <ul class="metasync-promo-benefits">
1823 <li><span class="promo-check">&#10003;</span> <?php echo esc_html($otto_name); ?> hands-free on-page SEO</li>
1824 <li><span class="promo-check">&#10003;</span> Real-time keyword tracking</li>
1825 <li><span class="promo-check">&#10003;</span> Automated schema markup</li>
1826 <li><span class="promo-check">&#10003;</span> Instant Google indexing</li>
1827 </ul>
1828 <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1829 Connect Now
1830 </a>
1831 </div>
1832 <?php else: ?>
1833 <!-- Connected feature highlights -->
1834 <div class="metasync-promo-card metasync-promo-card--accent">
1835 <div class="metasync-promo-card-header">
1836 <div class="metasync-promo-card-icon">
1837 <span class="dashicons dashicons-performance"></span>
1838 </div>
1839 <div>
1840 <h3><?php echo esc_html($otto_name); ?> Active</h3>
1841 </div>
1842 </div>
1843 <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1844 <ul class="metasync-promo-benefits">
1845 <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1846 <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1847 <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1848 </ul>
1849 <?php if ($is_default_brand): ?>
1850 <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1851 View <?php echo esc_html($plugin_name); ?> Dashboard
1852 </a>
1853 <?php elseif (!empty($whitelabel['domain'])): ?>
1854 <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1855 View <?php echo esc_html($plugin_name); ?> Dashboard
1856 </a>
1857 <?php endif; ?>
1858 </div>
1859 <?php endif; ?>
1860
1861 <?php
1862 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1863 $show_quick_links = $is_default_brand || !empty($custom_links);
1864 if ($show_quick_links):
1865 ?>
1866 <!-- Quick links card -->
1867 <div class="metasync-promo-card">
1868 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1869 <ul class="metasync-promo-links">
1870 <?php if ($is_default_brand): ?>
1871 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1872 <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>
1873 <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>
1874 <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>
1875 <?php else: ?>
1876 <?php foreach ($custom_links as $link): ?>
1877 <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>
1878 <?php endforeach; ?>
1879 <?php endif; ?>
1880 </ul>
1881 </div>
1882 <?php endif; ?>
1883
1884 <?php
1885 }
1886 }
1887