PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 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 All 141 releases
← All changes | includes/class-metasync-admin-pages.php +111 -468 2.6.10 → 2.7.1 View file →
@@ -217,10 +217,15 @@
217 217 // ------------------------------------------------------------------
218 218
219 219 public function create_admin_settings_page()
220 220 {
221 - $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
221 + if (isset($_REQUEST['metasync_password_reset'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the reset form verifies its own nonce
222 + Metasync_Admin_Ajax::instance()->render_password_reset_page();
223 + return;
224 + }
222 225
226 + $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
227 +
223 228 $whitelabel_settings = Metasync::get_whitelabel_settings();
224 229
225 230 $user_password = $whitelabel_settings['settings_password'] ?? '';
226 231 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
@@ -245,9 +250,9 @@
245 250
246 251 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
247 252 $password_validated = $auth->has_access();
248 253
249 - if (isset($_POST['whitelabel_password_submit']) && !$password_validated) {
254 + if (isset($_POST['whitelabel_password_submit']) && !$password_validated) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only access check for a display message
250 255 $password_error = 'Incorrect password. Please try again.';
251 256 }
252 257 }
253 258
@@ -333,8 +338,9 @@
333 338 </button>
334 339 </div>
335 340 </form>
336 341
342 + <?php if (current_user_can('manage_options')): ?>
337 343 <div style="text-align: center; margin-top: 20px;">
338 344 <a href="#" id="metasync-forgot-password-link" style="color: #2271b1; text-decoration: none; font-size: 14px;">
339 345 Forgot Password?
340 346 </a>
@@ -339,64 +345,12 @@
339 345 Forgot Password?
340 346 </a>
341 347 <div id="metasync-recovery-message" style="margin-top: 15px; padding: 12px; border-radius: 6px; display: none;"></div>
342 348 </div>
349 + <?php endif; ?>
343 350
344 351 </div>
345 352
346 - <script>
347 - jQuery(document).ready(function($) {
348 - $('#whitelabel_password').focus();
349 -
350 - $('#whitelabel_password').on('keypress', function(e) {
351 - if (e.which === 13) {
352 - $(this).closest('form').submit();
353 - }
354 - });
355 -
356 - $('#metasync-forgot-password-link').on('click', function(e) {
357 - e.preventDefault();
358 -
359 - var $link = $(this);
360 - var $message = $('#metasync-recovery-message');
361 -
362 - $link.css('pointer-events', 'none').css('opacity', '0.6');
363 - $message.removeClass('success error').hide();
364 - $message.html('⏳ Sending recovery email...').css('background', '#f0f6fc').css('color', '#0c5ba5').css('border', '1px solid #cfe2f3').fadeIn(200);
365 -
366 - $.ajax({
367 - url: ajaxurl,
368 - type: 'POST',
369 - data: {
370 - action: 'metasync_recover_password',
371 - nonce: '<?php echo wp_create_nonce('metasync_recover_password_nonce'); ?>'
372 - },
373 - success: function(response) {
374 - $link.css('pointer-events', 'auto').css('opacity', '1');
375 -
376 - if (response.success) {
377 - $message.addClass('success').html('✅ ' + response.data.message)
378 - .css('background', '#d4edda')
379 - .css('color', '#155724')
380 - .css('border', '1px solid #c3e6cb');
381 - } else {
382 - $message.addClass('error').html('❌ ' + response.data.message)
383 - .css('background', '#f8d7da')
384 - .css('color', '#721c24')
385 - .css('border', '1px solid #f5c6cb');
386 - }
387 - },
388 - error: function() {
389 - $link.css('pointer-events', 'auto').css('opacity', '1');
390 - $message.addClass('error').html('❌ An error occurred. Please try again.')
391 - .css('background', '#f8d7da')
392 - .css('color', '#721c24')
393 - .css('border', '1px solid #f5c6cb');
394 - }
395 - });
396 - });
397 - });
398 - </script>
399 353 <?php
400 354 return;
401 355 } elseif (!$password_protection_enabled) {
402 356 ?>
@@ -410,8 +364,9 @@
410 364 }
411 365 ?>
412 366
413 367 <form method="post" action="options.php?tab=<?php echo esc_attr( $active_tab ); ?>" id="metaSyncGeneralSetting">
368 + <input type="hidden" name="metasync_pw_reset_generation" value="<?php echo esc_attr((string) (int) get_option('metasync_pw_reset_generation', 0)); ?>" />
414 369 <?php
415 370 settings_fields(Metasync_Admin::option_group);
416 371
417 372 wp_nonce_field('meta_sync_general_setting_nonce', 'meta_sync_nonce');
@@ -439,14 +394,17 @@
439 394
440 395 <?php
441 396 $this->admin->render_accordion_sections(Metasync_Admin::$page_slug . '_general');
442 397 ?>
398 + <p style="margin: 20px 0 0 0; font-size: 13px; color: var(--dashboard-text-secondary);">
399 + Breadcrumb settings now live on their own page: <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-breadcrumbs')); ?>">Breadcrumbs</a>.
400 + </p>
443 401 </div>
444 402
445 403 <div class="dashboard-card">
446 - <h2>Content Genius Synchronization</h2>
404 + <h2>Content Genius Synchronization<?php Metasync::render_tooltip_icon('cg_sync_section', sprintf('Syncing sends this site\'s categories and authors to %s, so Content Genius can assign the right category and author when publishing articles to this site.', Metasync::get_effective_plugin_name())); ?></h2>
447 405 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Sync your categories and user/author data with <?php echo esc_html(Metasync::get_effective_plugin_name()); ?>.</p>
448 - <button type="button" class="button button-primary" id="sendAuthToken" data-toggle="tooltip" data-placement="top" title="Sync Categories and User">
406 + <button type="button" class="button button-primary" id="sendAuthToken">
449 407 Sync Now
450 408 </button>
451 409 </div>
452 410
@@ -465,14 +423,15 @@
465 423 ?>
466 424 <div class="dashboard-card">
467 425 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
468 426 <div>
469 - <h2>White Label Branding</h2>
427 + <h2>White Label Branding<?php Metasync::render_tooltip_icon('wl_branding_section', 'Replace this plugin\'s name, logo, and links with your own so clients see your brand instead of Search Atlas. Set a password below to stop clients from changing it back.'); ?></h2>
470 428 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Customize the plugin appearance with your own branding and logo.</p>
471 429 </div>
472 430 <?php if ($password_protection_enabled && $is_authenticated): ?>
473 431 <div>
474 432 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('whitelabel'); ?>
433 + <?php Metasync::render_tooltip_icon('wl_lock_section', 'Re-lock this section now. You will need the password to open it again.'); ?>
475 434 </div>
476 435 <?php endif; ?>
477 436 </div>
478 437
@@ -493,9 +452,9 @@
493 452 <!-- Color Palette Customization section -->
494 453 <div class="dashboard-card" id="metasync-color-palette-section">
495 454 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
496 455 <div>
497 - <h2>Color Palette</h2>
456 + <h2>Color Palette<?php Metasync::render_tooltip_icon('wl_color_palette', 'These colors control this plugin\'s admin dashboard theme only. Changes apply immediately here and never affect what your site\'s visitors see on the frontend.'); ?></h2>
498 457 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Customize the plugin color scheme for both dark and light themes. Changes preview instantly.</p>
499 458 </div>
500 459 </div>
501 460
@@ -738,9 +697,9 @@
738 697 </script>
739 698
740 699 <!-- Export Whitelabel Settings section -->
741 700 <div class="dashboard-card">
742 - <h2>Export Whitelabel Plugin</h2>
701 + <h2>Export Whitelabel Plugin<?php Metasync::render_tooltip_icon('wl_export_plugin', 'Downloads a copy of this plugin with your branding already baked in, ready to upload and install on a client\'s site. All your white-label settings are applied automatically on activation.'); ?></h2>
743 702 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Export the entire plugin with all whitelabel settings pre-configured. This creates a complete plugin zip file ready for installation on another WordPress site.</p>
744 703 <button type="button" class="button button-primary" id="metasync-export-whitelabel-btn" style="padding: 10px 20px; font-size: 14px; font-weight: 600;">
745 704 Export Plugin with Whitelabel Settings
746 705 </button>
@@ -887,10 +846,8 @@
887 846 originalCheckboxState = hideSettingsCheckbox.is(':checked');
888 847 }, 0);
889 848 });
890 849
891 - var storedPassword = '<?php echo esc_js($whitelabel_settings['settings_password'] ?? ''); ?>';
892 -
893 850 passwordField.on('keydown', function(e) {
894 851 var currentValue = $(this).val();
895 852
896 853 if (hideSettingsCheckbox.is(':checked') &&
@@ -920,42 +877,8 @@
920 877 $('#metasync-custom-modal').fadeOut(200);
921 878 }
922 879 });
923 880
924 - $('#metasync-export-whitelabel-btn').on('click', function(e) {
925 - e.preventDefault();
926 -
927 - var $button = $(this);
928 - var originalText = $button.html();
929 -
930 - $button.prop('disabled', true).html('⏳ Exporting...');
931 -
932 - var form = $('<form>', {
933 - 'method': 'POST',
934 - 'action': '<?php echo esc_js(admin_url('admin-post.php')); ?>',
935 - 'target': '_blank'
936 - });
937 -
938 - form.append($('<input>', {
939 - 'type': 'hidden',
940 - 'name': 'action',
941 - 'value': 'metasync_export_whitelabel_settings'
942 - }));
943 -
944 - form.append($('<input>', {
945 - 'type': 'hidden',
946 - 'name': '_wpnonce',
947 - 'value': '<?php echo wp_create_nonce('metasync_export_whitelabel'); ?>'
948 - }));
949 -
950 - $('body').append(form);
951 - form.submit();
952 -
953 - setTimeout(function() {
954 - form.remove();
955 - $button.prop('disabled', false).html(originalText);
956 - }, 2000);
957 - });
958 881 });
959 882 </script>
960 883 <?php
961 884 } elseif ($active_tab == 'advanced') {
@@ -985,10 +908,10 @@
985 908 </div>
986 909 <?php endif; ?>
987 910
988 911 <?php
989 - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
990 - $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1';
912 + if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice
913 + $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice
991 914 ?>
992 915 <div class="notice notice-success inline" style="margin-bottom: 20px;">
993 916 <p>
994 917 <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
@@ -995,9 +918,9 @@
995 918 </p>
996 919 </div>
997 920 <?php endif; ?>
998 921
999 - <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): ?>
922 + <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1000 923 <div class="notice notice-info inline" style="margin-bottom: 20px;">
1001 924 <p>
1002 925 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
1003 926 </p>
@@ -1003,9 +926,9 @@
1003 926 </p>
1004 927 </div>
1005 928 <?php endif; ?>
1006 929
1007 - <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): ?>
930 + <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1008 931 <div class="notice notice-success inline" style="margin-bottom: 20px;">
1009 932 <p>
1010 933 <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours.
1011 934 </p>
@@ -1011,9 +934,9 @@
1011 934 </p>
1012 935 </div>
1013 936 <?php endif; ?>
1014 937
1015 - <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): ?>
938 + <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1016 939 <div class="notice notice-error inline" style="margin-bottom: 20px;">
1017 940 <p>
1018 941 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
1019 942 </p>
@@ -1194,22 +1117,22 @@
1194 1117 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1195 1118
1196 1119 html += '<div class="test-result-item ' + statusClass + '">';
1197 1120 html += '<div class="test-result-header">';
1198 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1121 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1199 1122 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1200 1123 html += '</div>';
1201 1124
1202 1125 html += '<div class="test-result-details">';
1203 - html += '<p><strong>Response Time:</strong> ' + result.response_time + '</p>';
1204 - html += '<p><strong>Status:</strong> ' + result.status + '</p>';
1126 + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
1127 + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
1205 1128
1206 1129 if (result.status_code) {
1207 - html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + result.status_code + '</span></p>';
1130 + html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>';
1208 1131 }
1209 1132
1210 1133 if (result.error) {
1211 - html += '<p><strong>Error:</strong> <span class="error-message">' + result.error + '</span></p>';
1134 + html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>';
1212 1135 }
1213 1136
1214 1137 if (result.body) {
1215 1138 html += '<p><strong>Response Body:</strong></p>';
@@ -1219,9 +1142,9 @@
1219 1142 if (result.headers && Object.keys(result.headers).length > 0) {
1220 1143 html += '<p><strong>Response Headers:</strong></p>';
1221 1144 html += '<pre class="response-headers">';
1222 1145 Object.keys(result.headers).forEach(function(key) {
1223 - html += key + ': ' + result.headers[key] + '\n';
1146 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1224 1147 });
1225 1148 html += '</pre>';
1226 1149 }
1227 1150
@@ -1234,9 +1157,9 @@
1234 1157 html += '<p><strong>Parsed Response:</strong></p>';
1235 1158 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1236 1159 }
1237 1160
1238 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1161 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1239 1162 html += '</div>';
1240 1163 html += '</div>';
1241 1164 });
1242 1165
@@ -1251,9 +1174,9 @@
1251 1174 '>': '&gt;',
1252 1175 '"': '&quot;',
1253 1176 '\'': '&#039;'
1254 1177 };
1255 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1178 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1256 1179 }
1257 1180 });
1258 1181 </script>
1259 1182
@@ -1386,41 +1309,41 @@
1386 1309 }
1387 1310 </style>
1388 1311
1389 1312 <?php if ($active_tab === 'advanced'): ?>
1390 - <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): ?>
1313 + <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1391 1314 <div class="notice notice-success is-dismissible">
1392 1315 <p><strong>✅ Success!</strong> All plugin settings have been cleared successfully and a new Plugin Auth Token has been generated. Please reconfigure the plugin as needed.</p>
1393 1316 </div>
1394 1317 <?php endif; ?>
1395 1318
1396 - <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): ?>
1319 + <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1397 1320 <div class="notice notice-error is-dismissible">
1398 1321 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1399 1322 </div>
1400 1323 <?php endif; ?>
1401 1324
1402 - <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): ?>
1325 + <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1403 1326 <div class="notice notice-success is-dismissible">
1404 1327 <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p>
1405 1328 </div>
1406 1329 <?php endif; ?>
1407 1330
1408 - <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): ?>
1331 + <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1409 1332 <div class="notice notice-error is-dismissible">
1410 1333 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1411 1334 </div>
1412 1335 <?php endif; ?>
1413 1336
1414 - <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): ?>
1337 + <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1415 1338 <div class="notice notice-success is-dismissible">
1416 1339 <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p>
1417 1340 </div>
1418 1341 <?php endif; ?>
1419 1342
1420 - <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): ?>
1343 + <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1421 1344 <div class="notice notice-error is-dismissible">
1422 - <p><strong>❌ Error!</strong> Failed to save plugin access roles. <?php echo isset($_GET['message']) ? esc_html(urldecode($_GET['message'])) : 'Please try again.'; ?></p>
1345 + <p><strong>❌ Error!</strong> Failed to save plugin access roles. <?php echo isset($_GET['message']) ? esc_html(urldecode($_GET['message'])) : 'Please try again.'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only error text, escaped on output ?></p>
1423 1346 </div>
1424 1347 <?php endif; ?>
1425 1348
1426 1349 <?php endif; ?>
@@ -1454,8 +1377,15 @@
1454 1377 }
1455 1378
1456 1379 public function create_admin_robots_txt_page()
1457 1380 {
1381 + // The form below rewrites the site's physical robots.txt — restrict
1382 + // that to site admins, not just anyone who can open the page via
1383 + // plugin_access_roles. The nonce is CSRF protection, not authorization.
1384 + if (!empty($_POST) && !current_user_can('manage_options')) {
1385 + wp_die(esc_html__('Sorry, you are not allowed to manage robots.txt settings.', 'metasync'));
1386 + }
1387 +
1458 1388 ?>
1459 1389 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1460 1390
1461 1391 <?php
@@ -1500,9 +1430,11 @@
1500 1430 echo '<div class="notice notice-error"><p>' . esc_html($current_content->get_error_message()) . '</p></div>';
1501 1431 $current_content = '';
1502 1432 }
1503 1433
1504 - $backups = $robots_txt->get_backup_history(10);
1434 + // First page of backups; the Backup History panel is paginated so the
1435 + // list always reflects the full stored state and its true total.
1436 + $backups = $robots_txt->get_backup_history(Metasync_Robots_Txt_Database::BACKUPS_PER_PAGE, 0);
1505 1437
1506 1438 $file_exists = $robots_txt->file_exists();
1507 1439 $is_writable = $robots_txt->is_writable();
1508 1440
@@ -1516,80 +1448,8 @@
1516 1448 {
1517 1449 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1518 1450 }
1519 1451
1520 - public function create_admin_xml_sitemap_page()
1521 - {
1522 - require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
1523 -
1524 - $sitemap_generator = new Metasync_Sitemap_Generator();
1525 -
1526 - if (isset($_POST['metasync_sitemap_nonce'])) {
1527 - check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
1528 -
1529 - if (isset($_POST['generate_sitemap'])) {
1530 - $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
1531 -
1532 - $result = $sitemap_generator->generate_sitemap();
1533 -
1534 - if (is_wp_error($result)) {
1535 - error_log('[MetaSync] Sitemap generation failed: ' . $result->get_error_message());
1536 - echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed. Please try again or check the error logs.', 'metasync') . '</p></div>';
1537 - } else {
1538 - $message = esc_html__('Sitemap generated successfully!', 'metasync');
1539 - if ($disabled_plugins) {
1540 - $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
1541 - }
1542 -
1543 - $robots_result = get_transient('metasync_sitemap_robots_updated');
1544 - if ($robots_result && $robots_result['success']) {
1545 - if ($robots_result['action'] === 'added') {
1546 - $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
1547 - } elseif ($robots_result['action'] === 'updated') {
1548 - $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
1549 - } elseif ($robots_result['action'] === 'created') {
1550 - $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
1551 - }
1552 - delete_transient('metasync_sitemap_robots_updated');
1553 - }
1554 -
1555 - echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
1556 - }
1557 - } elseif (isset($_POST['enable_auto_update'])) {
1558 - update_option('metasync_sitemap_auto_update', true);
1559 - $sitemap_generator->setup_auto_update_hooks();
1560 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
1561 - } elseif (isset($_POST['disable_auto_update'])) {
1562 - update_option('metasync_sitemap_auto_update', false);
1563 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
1564 - } elseif (isset($_POST['delete_sitemap'])) {
1565 - $deleted = $sitemap_generator->delete_sitemap();
1566 - if ($deleted) {
1567 - update_option('metasync_sitemap_auto_update', false);
1568 - echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>';
1569 - } else {
1570 - echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete sitemap. The file may not exist or is not writable.', 'metasync') . '</p></div>';
1571 - }
1572 - } elseif (isset($_POST['enable_other_sitemaps'])) {
1573 - $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
1574 - if ($enabled_plugins) {
1575 - echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
1576 - } else {
1577 - echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
1578 - }
1579 - }
1580 - }
1581 -
1582 - $sitemap_exists = $sitemap_generator->sitemap_exists();
1583 - $sitemap_url = $sitemap_generator->get_sitemap_url();
1584 - $url_count = $sitemap_generator->count_urls();
1585 - $last_generated = $sitemap_generator->get_last_generated_time();
1586 - $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
1587 - $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
1588 -
1589 - require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
1590 - }
1591 -
1592 1452 public function create_admin_custom_pages_page()
1593 1453 {
1594 1454 ?>
1595 1455 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
@@ -1640,12 +1500,14 @@
1640 1500 }
1641 1501
1642 1502 public function create_admin_breadcrumbs_page()
1643 1503 {
1644 - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1645 1504 ?>
1646 1505 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1647 1506
1507 + <?php /* options.php redirects back here with ?settings-updated=true; without this call the save confirmation never prints */ ?>
1508 + <?php settings_errors(); ?>
1509 +
1648 1510 <form method="post" action="options.php">
1649 1511 <div class="dashboard-card">
1650 1512 <h2>Breadcrumbs Settings</h2>
1651 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
@@ -1650,9 +1512,9 @@
1650 1512 <h2>Breadcrumbs Settings</h2>
1651 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1652 1514 <?php
1653 1515 settings_fields(Metasync_Admin::option_group);
1654 - do_settings_sections($page_slug);
1516 + Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1655 1517 submit_button();
1656 1518 ?>
1657 1519 </div>
1658 1520 </form>
@@ -1662,23 +1524,53 @@
1662 1524 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Breadcrumbs are not inserted automatically. Use one of the methods below to place them on your site.</p>
1663 1525
1664 1526 <h3 style="margin-top: 0;">Option 1 — Shortcode</h3>
1665 1527 <p style="color: var(--dashboard-text-secondary);">Paste the shortcode inside any page, post, or widget:</p>
1666 - <pre style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text, #e0e0e0); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">[metasync_breadcrumb]</pre>
1528 + <div class="metasync-copy-wrap"><pre class="metasync-copy-snippet" style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text-primary); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">[metasync_breadcrumb]</pre><span class="metasync-copy-badge">Copy</span></div>
1667 1529
1668 1530 <h3>Option 2 — PHP Template Function</h3>
1669 1531 <p style="color: var(--dashboard-text-secondary);">Call the helper function directly inside your theme template files (e.g. <code>single.php</code>, <code>page.php</code>, <code>header.php</code>):</p>
1670 - <pre style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text, #e0e0e0); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">&lt;?php metasync_breadcrumb(); ?&gt;</pre>
1532 + <div class="metasync-copy-wrap"><pre class="metasync-copy-snippet" style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text-primary); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">&lt;?php metasync_breadcrumb(); ?&gt;</pre><span class="metasync-copy-badge">Copy</span></div>
1671 1533
1672 1534 <h3>Option 3 — PHP Return Value</h3>
1673 1535 <p style="color: var(--dashboard-text-secondary);">Get the breadcrumb HTML as a string to manipulate before output:</p>
1674 - <pre style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text, #e0e0e0); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">$breadcrumbs = new Metasync_Breadcrumbs();
1675 -echo $breadcrumbs-&gt;render_breadcrumb_html();</pre>
1536 + <div class="metasync-copy-wrap"><pre class="metasync-copy-snippet" style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text-primary); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">$breadcrumbs = new Metasync_Breadcrumbs();
1537 +echo $breadcrumbs-&gt;render_breadcrumb_html();</pre><span class="metasync-copy-badge">Copy</span></div>
1676 1538
1677 1539 <p style="color: var(--dashboard-text-secondary); margin-top: 16px; font-size: 12px;">
1678 1540 <strong>Tip:</strong> You can also pass a <code>post_id</code> argument to generate breadcrumbs for a specific post outside of a template context:<br>
1679 1541 <code>echo $breadcrumbs-&gt;render_breadcrumb_html(['post_id' =&gt; 123]);</code>
1680 1542 </p>
1543 + <?php /* click-to-copy for the code snippets above */ ?>
1544 + <style>
1545 + .metasync-copy-wrap { position: relative; }
1546 + .metasync-copy-snippet { cursor: pointer; outline: 1px solid transparent; transition: outline-color .15s ease; }
1547 + .metasync-copy-wrap:hover .metasync-copy-snippet { outline-color: var(--dashboard-border, #374151); }
1548 + .metasync-copy-badge { position: absolute; top: 8px; right: 10px; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px; pointer-events: none; opacity: 0; transition: opacity .15s ease; background: var(--dashboard-border, #374151); color: var(--dashboard-text-primary, #fff); }
1549 + .metasync-copy-wrap:hover .metasync-copy-badge { opacity: .85; }
1550 + .metasync-copy-badge.metasync-copy-done { opacity: 1; background: var(--dashboard-gradient-primary, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); color: #fff; }
1551 + </style>
1552 + <script>
1553 + (function(){
1554 + if (window.__metasyncCopyInit) return; window.__metasyncCopyInit = true;
1555 + function fallback(text){ var ta=document.createElement('textarea'); ta.value=text; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.focus(); ta.select(); try{document.execCommand('copy');}catch(e){} document.body.removeChild(ta); }
1556 + function attach(){
1557 + document.querySelectorAll('.metasync-copy-snippet').forEach(function(pre){
1558 + if (pre.dataset.copyBound) return; pre.dataset.copyBound='1';
1559 + pre.setAttribute('title','Click to copy');
1560 + pre.addEventListener('click', function(){
1561 + var text = pre.textContent.trim();
1562 + var wrap = pre.closest('.metasync-copy-wrap') || pre.parentNode;
1563 + var badge = wrap.querySelector('.metasync-copy-badge');
1564 + var done = function(){ if(!badge) return; badge.textContent='Copied!'; badge.classList.add('metasync-copy-done'); clearTimeout(badge._t); badge._t=setTimeout(function(){ badge.textContent='Copy'; badge.classList.remove('metasync-copy-done'); },1500); };
1565 + if (navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done).catch(function(){ fallback(text); done(); }); }
1566 + else { fallback(text); done(); }
1567 + });
1568 + });
1569 + }
1570 + if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', attach); } else { attach(); }
1571 + })();
1572 + </script>
1681 1573 </div>
1682 1574 <?php $this->admin->render_layout_close(); ?>
1683 1575 <?php
1684 1576 }
@@ -1689,8 +1581,10 @@
1689 1581 $this->admin->render_layout_open('Schema Markup', 'schema_markup', 'Configure global schema markup settings for your site. Organization and WebSite schemas are injected site-wide.');
1690 1582 ?>
1691 1583 <form method="post" action="options.php">
1692 1584 <div class="dashboard-card">
1585 + <h2>Schema Markup Settings</h2>
1586 + <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure the site-wide Organization and WebSite structured data injected into every page.</p>
1693 1587 <?php
1694 1588 settings_fields(Metasync_Admin::option_group);
1695 1589 do_settings_sections($page_slug);
1696 1590 submit_button('Save Changes');
@@ -1708,9 +1602,9 @@
1708 1602 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1709 1603
1710 1604 <form method="post" action="options.php">
1711 1605 <div class="dashboard-card">
1712 - <h2>🏢 Local Business Configuration</h2>
1606 + <h2>🏢 Local Business Configuration<?php Metasync::render_tooltip_icon('local_business_section', 'Fill in your business name, address, phone, hours, and category so search engines can show your business in local results and map listings. This generates LocalBusiness schema behind the scenes. Use your exact, consistent NAP (name/address/phone) details.'); ?></h2>
1713 1607 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1714 1608 <?php
1715 1609 settings_fields(Metasync_Admin::option_group);
1716 1610 do_settings_sections($page_slug);
@@ -1742,24 +1636,20 @@
1742 1636 <?php $this->admin->render_layout_close(); ?>
1743 1637 <?php
1744 1638 }
1745 1639
1746 - public function create_admin_google_instant_index_page()
1747 - {
1748 - // Delegates to the admin class which uses the shared credentials view
1749 - $this->admin->create_admin_google_instant_index_page();
1750 - }
1751 -
1752 1640 public function create_admin_google_console_page()
1753 1641 {
1754 - ?>
1755 - <?php $this->admin->render_layout_open('Google Console', 'google_console', ''); ?>
1756 -
1757 - <?php
1758 - google_index_direct()->show_google_instant_indexing_console();
1759 - ?>
1760 - <?php $this->admin->render_layout_close(); ?>
1761 - <?php
1642 + $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1643 + $google = function_exists('google_index_direct') ? google_index_direct() : null;
1644 + if (is_null($google)) {
1645 + $service_info = ['error' => 'Module not loaded'];
1646 + } else {
1647 + $service_info = $google->get_service_account_info();
1648 + }
1649 + $is_configured = !isset($service_info['error']);
1650 + include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
1651 + $this->admin->render_layout_close();
1762 1652 }
1763 1653
1764 1654 public function create_admin_bing_console_page()
1765 1655 {
@@ -1774,33 +1664,8 @@
1774 1664 <?php $this->admin->render_layout_close(); ?>
1775 1665 <?php
1776 1666 }
1777 1667
1778 - public function create_admin_optimal_settings_page()
1779 - {
1780 - ?>
1781 - <?php $this->admin->render_layout_open('Optimal Settings', 'optimal_settings', 'Apply recommended SEO settings automatically.'); ?>
1782 -
1783 - <div class="dashboard-card">
1784 - <h2>Site Compatibility Status</h2>
1785 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Check your site's compatibility with optimal <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> settings.</p>
1786 - <?php
1787 - $optimal_settings = new Metasync_Optimal_Settings();
1788 - $optimal_settings->site_compatible_status_view();
1789 - ?>
1790 - </div>
1791 -
1792 - <div class="dashboard-card">
1793 - <h2>Optimization Settings</h2>
1794 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure optimization settings for best performance.</p>
1795 - <?php
1796 - $this->optimization_settings_options();
1797 - ?>
1798 - </div>
1799 - <?php $this->admin->render_layout_close(); ?>
1800 - <?php
1801 - }
1802 -
1803 1668 public function create_admin_global_settings_page()
1804 1669 {
1805 1670 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1806 1671 ?>
@@ -1895,21 +1760,9 @@
1895 1760 <?php $this->admin->render_layout_close(); ?>
1896 1761 <?php
1897 1762 }
1898 1763
1899 - public function optimization_settings_options()
1900 - {
1901 - $page_slug = Metasync_Admin::$page_slug . '_optimal-settings';
1902 - $site_info_slug = Metasync_Admin::$page_slug . '_site-info-settings';
1903 1764
1904 - printf('<form method="post" action="options.php">');
1905 - settings_fields(Metasync_Admin::option_group);
1906 - do_settings_sections($page_slug);
1907 - do_settings_sections($site_info_slug);
1908 - submit_button();
1909 - printf('</form>');
1910 - }
1911 -
1912 1765 public function create_admin_error_logs_page()
1913 1766 {
1914 1767 ?>
1915 1768 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
@@ -1951,177 +1804,15 @@
1951 1804 <?php $this->admin->render_layout_close(); ?>
1952 1805 <?php
1953 1806 }
1954 1807
1955 - public function create_admin_sync_log_page()
1956 - {
1957 - $sync_db = new Metasync_Sync_History_Database();
1958 -
1959 - if (wp_doing_ajax()) {
1960 - $this->handle_sync_log_ajax();
1961 - return;
1962 - }
1963 -
1964 - $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
1965 - $per_page = 10;
1966 - $offset = ($page - 1) * $per_page;
1967 -
1968 - $filters = [
1969 - 'date_range' => isset($_GET['date_range']) ? sanitize_text_field($_GET['date_range']) : '',
1970 - 'status' => isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '',
1971 - ];
1808 + // NOTE: The Changes Log (sync log) page is rendered by
1809 + // Metasync_Admin::create_admin_sync_log_page() in
1810 + // admin/class-metasync-admin.php, which is the callback registered in
1811 + // class-metasync-admin-navigation.php. A former duplicate of that method
1812 + // lived here but was never wired to a menu and has been removed to avoid
1813 + // drift.
1972 1814
1973 - $date_range = $filters['date_range'];
1974 - $wp_now_ts = current_time('timestamp');
1975 - $date_from = '';
1976 - $date_to = '';
1977 -
1978 - if (!empty($date_range)) {
1979 - $date_to = date('Y-m-d H:i:s', $wp_now_ts);
1980 -
1981 - if ($date_range === 'today') {
1982 - $start_ts = strtotime('today', $wp_now_ts);
1983 - $date_from = date('Y-m-d H:i:s', $start_ts);
1984 - } elseif ($date_range === 'yesterday') {
1985 - $start_ts = strtotime('yesterday', $wp_now_ts);
1986 - $end_ts = strtotime('today', $wp_now_ts) - 1;
1987 - $date_from = date('Y-m-d H:i:s', $start_ts);
1988 - $date_to = date('Y-m-d H:i:s', $end_ts);
1989 - } elseif ($date_range === 'this_week') {
1990 - $start_of_week = (int) get_option('start_of_week', 1);
1991 - $day_of_week = (int) date('w', $wp_now_ts);
1992 - $delta_days = ($day_of_week - $start_of_week + 7) % 7;
1993 - $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
1994 - $date_from = date('Y-m-d H:i:s', $start_ts);
1995 - } elseif ($date_range === 'this_month') {
1996 - $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
1997 - $date_from = date('Y-m-d H:i:s', $start_ts);
1998 - } elseif ($date_range === 'all') {
1999 - // no bounds
2000 - }
2001 - }
2002 -
2003 - if (!empty($date_from)) {
2004 - $filters['date_from'] = $date_from;
2005 - }
2006 - if (!empty($date_to)) {
2007 - $filters['date_to'] = $date_to;
2008 - }
2009 -
2010 - $filters = array_filter($filters);
2011 -
2012 - $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
2013 - $total_records = $sync_db->get_count($filters);
2014 - $total_pages = ceil($total_records / $per_page);
2015 -
2016 - $stats = $sync_db->get_statistics();
2017 -
2018 - ?>
2019 - <?php $this->admin->render_layout_open('Changes Log', 'sync_log', 'Track all SEO changes made by the plugin.'); ?>
2020 -
2021 - <div class="dashboard-card">
2022 - <div class="sync-log-header">
2023 - <div class="sync-log-title-section">
2024 - <h2>Changes Log</h2>
2025 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">Recent content synchronizations from external tools.</p>
2026 - </div>
2027 -
2028 - <!-- Filters - Right aligned -->
2029 - <div class="sync-log-filters">
2030 - <form method="get" class="sync-filters-form" onchange="this.submit()">
2031 - <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
2032 -
2033 - <select name="date_range" class="sync-filter-select">
2034 - <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
2035 - <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
2036 - <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
2037 - <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
2038 - <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
2039 - </select>
2040 -
2041 - <select name="status" class="sync-filter-select">
2042 - <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
2043 - <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
2044 - <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
2045 - </select>
2046 - </form>
2047 - </div>
2048 - </div>
2049 -
2050 - <!-- Sync History List -->
2051 - <div class="sync-log-list">
2052 - <?php if (empty($sync_records)): ?>
2053 - <div class="sync-log-empty">
2054 - <div class="sync-log-empty-icon"><span class="dashicons dashicons-list-view" style="font-size:48px;width:48px;height:48px;color:var(--dashboard-text-secondary);"></span></div>
2055 - <h3>No sync records found</h3>
2056 - <p>Sync records will appear here when content/pages receive new updates.</p>
2057 - </div>
2058 - <?php else: ?>
2059 - <?php foreach ($sync_records as $record): ?>
2060 - <div class="sync-log-item">
2061 - <div class="sync-log-icon">
2062 - <div class="sync-icon-circle">
2063 - <span class="dashicons dashicons-media-default sync-icon"></span>
2064 - </div>
2065 - </div>
2066 -
2067 - <div class="sync-log-content">
2068 - <div class="sync-log-title"><?php echo esc_html($record->title); ?>
2069 - <?php if (!empty($record->url)): ?>
2070 - <a href="<?php echo esc_url($record->url); ?>" target="_blank" rel="noopener" title="Open URL" style="margin-left:8px; text-decoration:none;"><span class="dashicons dashicons-external" style="font-size:14px;width:14px;height:14px;vertical-align:middle;"></span></a>
2071 - <?php endif; ?>
2072 - </div>
2073 - <div class="sync-log-meta">
2074 - <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?>
2075 - </div>
2076 - </div>
2077 -
2078 - <div class="sync-log-status">
2079 - <?php if ($record->status === 'published' || $record->status === 'publish'): ?>
2080 - <span class="sync-status-badge sync-status-published">
2081 - <span class="sync-status-icon">✓</span>
2082 - Published
2083 - </span>
2084 - <?php else: ?>
2085 - <span class="sync-status-badge sync-status-draft">
2086 - <span class="sync-status-icon">i</span>
2087 - Draft
2088 - </span>
2089 - <?php endif; ?>
2090 - </div>
2091 - </div>
2092 - <?php endforeach; ?>
2093 - <?php endif; ?>
2094 - </div>
2095 -
2096 - <!-- Pagination -->
2097 - <?php if ($total_pages > 1): ?>
2098 - <div class="sync-log-pagination">
2099 - <div class="sync-log-pagination-info">
2100 - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?>
2101 - </div>
2102 -
2103 - <div class="sync-log-pagination-controls">
2104 - <?php if ($page > 1): ?>
2105 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) - 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">‹</a>
2106 - <?php endif; ?>
2107 -
2108 - <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
2109 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
2110 - class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
2111 - <?php endfor; ?>
2112 -
2113 - <?php if ($page < $total_pages): ?>
2114 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) + 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">›</a>
2115 - <?php endif; ?>
2116 - </div>
2117 - </div>
2118 - <?php endif; ?>
2119 - </div>
2120 - <?php $this->admin->render_layout_close(); ?>
2121 - <?php
2122 - }
2123 -
2124 1815 public function creat_error_Logs_List()
2125 1816 {
2126 1817 // printf('<h1> Error Logs </h1>');
2127 1818 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
@@ -2150,59 +1841,11 @@
2150 1841 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2151 1842 }
2152 1843
2153 1844 // ------------------------------------------------------------------
2154 - // Private helpers (copied from Metasync_Admin, used only by pages above)
1845 + // Private helpers
2155 1846 // ------------------------------------------------------------------
2156 -
2157 - private function time_elapsed_string($datetime, $full = false)
2158 - {
2159 - if(empty($datetime)){
2160 - return "";
2161 - }
2162 - # Stored timestamps use current_time('mysql') which is WP local time
2163 - # with no timezone marker. Defaulting DateTime to UTC produces a diff
2164 - # equal to the WP timezone offset (WP-350 QA: "4 hours ago" after a
2165 - # 4-minute sync on non-UTC sites).
2166 - $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
2167 - $now = new DateTime('now', $wp_tz);
2168 - $ago = new DateTime($datetime, $wp_tz);
2169 -
2170 - $diff = $now->diff($ago);
2171 -
2172 - $string = [
2173 - 'y' => 'year',
2174 - 'm' => 'month',
2175 - 'w' => 'week',
2176 - 'd' => 'day',
2177 - 'h' => 'hour',
2178 - 'i' => 'minute',
2179 - 's' => 'second',
2180 - ];
2181 -
2182 - foreach ($string as $k => &$v) {
2183 - if (isset($diff->$k) && $diff->$k) {
2184 - $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
2185 - } else {
2186 - unset($string[$k]);
2187 - }
2188 - }
2189 - if (!$full) $string = array_slice($string, 0, 1);
2190 - return $string ? implode(', ', $string) . ' ago' : 'just now';
2191 - }
2192 -
2193 - private function build_filter_query_string($filters)
2194 - {
2195 - $query_parts = [];
2196 - foreach ($filters as $key => $value) {
2197 - if (!empty($value)) {
2198 - $query_parts[] = $key . '=' . urlencode($value);
2199 - }
2200 - }
2201 - return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
2202 - }
2203 -
2204 - private function handle_sync_log_ajax()
2205 - {
2206 - wp_die();
2207 - }
1847 + // The former time_elapsed_string / build_filter_query_string /
1848 + // handle_sync_log_ajax helpers here were only used by the dead
1849 + // create_admin_sync_log_page() duplicate that has since been removed. The
1850 + // live equivalents live on Metasync_Admin.
2208 1851 }