PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.5
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.5
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 / admin / js / metasync-admin.js

metasync-admin.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.5, at admin/js/metasync-admin.js

3,164 lines 104.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function ($) {
2 'use strict';
3
4 /**
5 * All of the code for your admin-facing JavaScript source
6 * should reside in this file.
7 *
8 * Note: It has been assumed you will write jQuery code here, so the
9 * $ function reference has been prepared for usage within the scope
10 * of this function.
11 *
12 * This enables you to define handlers, for when the DOM is ready:
13 *
14 * $(function() {
15 *
16 * });
17 *
18 * When the window is loaded:
19 *
20 * $( window ).load(function() {
21 *
22 * });
23 *
24 * ...and/or other possibilities.
25 *
26 * Ideally, it is not considered best practise to attach more than a
27 * single DOM-ready or window-load handler for a particular page.
28 * Although scripts in the WordPress core, Plugins and Themes may be
29 * practising this, we should strive to set a better example in our own work.
30 */
31
32 // ========================================
33 // UTILITY FUNCTIONS (Consolidated to reduce duplication)
34 // ========================================
35
36 /**
37 * Get plugin name from config or use default
38 * @returns {string} Plugin name
39 */
40 function getPluginName() {
41 return window.MetasyncConfig && window.MetasyncConfig.pluginName ? window.MetasyncConfig.pluginName : 'Search Atlas';
42 }
43
44 /**
45 * Get OTTO name from config or use default
46 * @returns {string} OTTO name
47 */
48 function getOttoName() {
49 return window.MetasyncConfig && window.MetasyncConfig.ottoName ? window.MetasyncConfig.ottoName : 'OTTO';
50 }
51
52 /**
53 * Update integration status indicator in header
54 * @param {boolean} isIntegrated - Whether the integration is active
55 * @param {string} statusText - Status text to display
56 * @param {string} titleText - Tooltip text
57 */
58 function updateHeaderStatus(isIntegrated, statusText, titleText) {
59 var $statusIndicator = $('.metasync-integration-status');
60 if ($statusIndicator.length > 0) {
61 $statusIndicator.removeClass('integrated not-integrated warning');
62 if (statusText === 'Warning') {
63 $statusIndicator.addClass('warning');
64 } else if (isIntegrated) {
65 $statusIndicator.addClass('integrated');
66 } else {
67 $statusIndicator.addClass('not-integrated');
68 }
69 $statusIndicator.find('.status-text').text(statusText);
70 $statusIndicator.attr('title', titleText);
71 console.log('🔄 Updated header status to: ' + statusText);
72 }
73
74 // Keep compact header badge in sync with integration status
75 var $badge = $('.metasync-status');
76 if ($badge.length > 0) {
77 $badge.removeClass('connected disconnected warning');
78 if (isIntegrated) {
79 $badge.addClass('connected');
80 $badge.find('.status-text').text('Connected');
81 } else if (statusText === 'Warning') {
82 $badge.addClass('warning');
83 $badge.find('.status-text').text('Warning');
84 } else {
85 $badge.addClass('disconnected');
86 $badge.find('.status-text').text('Not Connected');
87 }
88 }
89
90 // Immediately sync admin bar toolbar status
91 var $adminBarContainer = $('#wp-admin-bar-searchatlas-status');
92 var $adminBarItem = $adminBarContainer.find('.ab-item');
93 if ($adminBarItem.length > 0) {
94 var allClasses = 'searchatlas-synced searchatlas-not-synced searchatlas-warning';
95 var pluginName = (window.MetasyncConfig && window.MetasyncConfig.pluginName) || 'SearchAtlas';
96
97 var targetEmoji, targetSvgCode, barClass, barTitle;
98 if (isIntegrated) {
99 targetEmoji = '\uD83D\uDFE2'; targetSvgCode = '1f7e2';
100 barClass = 'searchatlas-synced';
101 barTitle = pluginName + ' - Synced';
102 } else if (statusText === 'Warning') {
103 targetEmoji = '\uD83D\uDFE1'; targetSvgCode = '1f7e1';
104 barClass = 'searchatlas-warning';
105 barTitle = pluginName + ' - ' + titleText;
106 } else {
107 targetEmoji = '\uD83D\uDD34'; targetSvgCode = '1f534';
108 barClass = 'searchatlas-not-synced';
109 barTitle = pluginName + ' - ' + (titleText || 'Not Synced');
110 }
111
112 // Update emoji (SVG image or text fallback)
113 var emojiImg = $adminBarItem.find('img.emoji');
114 if (emojiImg.length > 0) {
115 emojiImg.attr('alt', targetEmoji);
116 emojiImg.attr('src', emojiImg.attr('src').replace(/1f7e2\.svg|1f534\.svg|1f7e1\.svg/, targetSvgCode + '.svg'));
117 } else {
118 var newHtml = $adminBarItem.html().replace(/\uD83D\uDFE2|\uD83D\uDD34|\uD83D\uDFE1/, targetEmoji);
119 if (!newHtml.includes(targetEmoji) && newHtml.includes(pluginName)) {
120 newHtml = newHtml.replace(pluginName, pluginName + ' ' + targetEmoji);
121 }
122 $adminBarItem.html(newHtml);
123 }
124
125 $adminBarContainer.removeClass(allClasses).addClass(barClass);
126 $adminBarContainer.attr('title', barTitle);
127 $adminBarItem.attr('title', barTitle);
128 }
129 }
130
131 /**
132 * Collect whitelabel form fields for submission
133 * @returns {string} Serialized whitelabel field data
134 */
135 function collectWhitelabelFields() {
136 var whitelabelFields = [];
137
138 // Text/URL fields
139 var logoField = $('input[name="metasync_options[whitelabel][logo]"]');
140 var domainField = $('input[name="metasync_options[whitelabel][domain]"]');
141 var passwordField = $('input[name="metasync_options[whitelabel][settings_password]"]');
142
143 if (logoField.length > 0 && logoField.val()) {
144 whitelabelFields.push('metasync_options[whitelabel][logo]=' + encodeURIComponent(logoField.val()));
145 }
146 if (domainField.length > 0 && domainField.val()) {
147 whitelabelFields.push('metasync_options[whitelabel][domain]=' + encodeURIComponent(domainField.val()));
148 }
149 if (passwordField.length > 0 && passwordField.val()) {
150 whitelabelFields.push('metasync_options[whitelabel][settings_password]=' + encodeURIComponent(passwordField.val()));
151 }
152
153 // Checkbox fields (handle both checked and unchecked)
154 var hideFields = ['hide_dashboard', 'hide_settings', 'hide_indexation_control',
155 'hide_redirections', 'hide_robots', 'hide_sync_log',
156 'hide_compatibility', 'hide_advanced'];
157
158 hideFields.forEach(function (fieldName) {
159 var checkbox = $('input[name="metasync_options[whitelabel][' + fieldName + ']"]');
160 if (checkbox.length > 0) {
161 var value = checkbox.is(':checked') ? '1' : '0';
162 whitelabelFields.push('metasync_options[whitelabel][' + fieldName + ']=' + value);
163 }
164 });
165
166 return whitelabelFields.join('&');
167 }
168
169 /**
170 * Display notice message in plugin area
171 * @param {string} type - Notice type: 'success' or 'error'
172 * @param {string} title - Notice title
173 * @param {string} message - Notice message
174 * @param {string} cssClass - Additional CSS class for the notice
175 * @param {number} autoHideDelay - Auto-hide delay in ms (0 = no auto-hide)
176 */
177 function showPluginNotice(type, title, message, cssClass, autoHideDelay) {
178 cssClass = cssClass || 'metasync-notice';
179 autoHideDelay = autoHideDelay || 0;
180
181 var noticeClass = type === 'success' ? 'notice-success' : 'notice-error';
182 var noticeHTML = '<div class="notice ' + noticeClass + ' is-dismissible ' + cssClass + '" style="margin: 20px 0; padding: 12px;">' +
183 '<p><strong>' + title + '</strong><br/>' + message + '</p>' +
184 '</div>';
185
186 // Remove existing notices of same class
187 $('.' + cssClass).remove();
188
189 // Insert between navigation menu and page content
190 var $navWrapper = $('.metasync-nav-wrapper');
191 if ($navWrapper.length > 0) {
192 $navWrapper.after(noticeHTML);
193 } else {
194 $('.metasync-dashboard-wrap').prepend(noticeHTML);
195 }
196
197 // Scroll to the top to ensure visibility
198 $('html, body').animate({ scrollTop: 0 }, 'slow');
199
200 // Auto-hide if delay specified
201 if (autoHideDelay > 0) {
202 setTimeout(function () {
203 $('.' + cssClass).fadeOut(300, function () {
204 $(this).remove();
205 });
206 }, autoHideDelay);
207 }
208 }
209
210 /**
211 * Prevent dashboard.js interference with button
212 * @param {jQuery} $button - Button element to protect
213 */
214 function preventDashboardInterference($button) {
215 $button.removeClass('dashboard-loading');
216 $button.addClass('no-loading metasync-sa-connect-protected');
217 $button.prop('disabled', false);
218 }
219
220 // ========================================
221 // ORIGINAL FUNCTIONS
222 // ========================================
223
224 function metasync_syncPostsAndPages() {
225 wp.ajax.post('metasync_send_customer_params', {})
226 .done(function (response) {
227 console.log(response);
228 });
229 }
230
231 function metasyncGenerateAPIKey() {
232 return Math.random().toString(36).substring(2, 15) +
233 Math.random().toString(36).substring(2, 15);
234 }
235
236 function metasyncLGLogin(user, pass) {
237 jQuery.post(ajaxurl, {
238 action: 'metasync_lglogin',
239 username: user, password: pass
240 }, function (response) {
241 if (typeof response.token !== 'undefined') {
242 $('#linkgraph_token').val(response.token);
243 $('#linkgraph_customer_id').val(response.customer_id);
244 $('.input.lguser,#lgerror').addClass('hidden');
245 localStorage.setItem('token', response.token);
246 } else {
247 $('#lgerror').html(`${response.detail} (${response.kind})`).removeClass('hidden');
248 }
249 }
250 );
251 }
252
253 function setToken() {
254 if ($('#linkgraph_token') && $('#linkgraph_token').val()) {
255 localStorage.setItem('token', $('#linkgraph_token').val());
256 }
257 }
258
259 // Search Atlas Connect functions
260 // Handles 1-click authentication to retrieve Search Atlas API key and Otto UUID.
261 // Does NOT create WordPress login sessions.
262 var saConnectPollingInterval = null;
263 var saConnectWindow = null;
264
265 function handleSearchAtlasConnect() {
266 var $button = $('#connect-searchatlas-btn');
267 // Only check for button (status/progress elements are created dynamically)
268 if (!$button.length) {
269 return;
270 }
271
272 // Enhanced loading state with spinner and CSS class (prevent dashboard.js conflicts)
273 $button.prop('disabled', true)
274 .addClass('connecting no-loading') // Add 'no-loading' to prevent dashboard.js interference
275 .removeClass('dashboard-loading') // Remove any existing dashboard loading
276 .html('<span class="metasync-sa-connect-loading"></span> Initializing...');
277
278 // Hide any existing status/progress containers (may not exist yet)
279 $('#sa-connect-status-message').hide();
280 $('.metasync-sa-connect-progress').hide();
281
282 // Initialize progress display immediately (no separate status message)
283 initializeProgressDisplay();
284
285 // Generate nonce for WordPress AJAX security
286 var ajaxNonce = metaSync.sa_connect_nonce || '';
287 if (!ajaxNonce) {
288 return;
289 }
290
291 // Make AJAX call to generate SSO URL
292 var ajaxUrl = ajaxurl || metaSync.ajax_url;
293
294
295 $.ajax({
296 url: ajaxUrl,
297 type: 'POST',
298 data: {
299 action: 'metasync_generate_connect_url',
300 nonce: ajaxNonce
301 },
302 timeout: 30000, // 30 second timeout
303 success: function (response) {
304
305
306 if (response.success) {
307
308 // Update button state
309 $button.removeClass('connecting dashboard-loading')
310 .addClass('authenticating no-loading') // Maintain no-loading class
311 .html('<span class="metasync-sa-connect-loading"></span> Opening Authentication...');
312
313
314
315 // Small delay for better UX (let user see the message)
316 setTimeout(function () {
317 console.log('🔍 Opening connect popup with URL:', response.data.connect_url);
318
319 // Detect mobile device for better experience
320 var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
321 var windowFeatures;
322
323
324
325 if (isMobile) {
326 // On mobile, open in same tab for better experience
327 showConnectInfo('📱 Mobile Authentication',
328 'Opening ' + getPluginName() + ' authentication. You\'ll be redirected back after logging in.');
329 window.location.href = response.data.connect_url;
330 return;
331 } else {
332 // Desktop: enhanced popup window
333 var screenWidth = window.screen.width;
334 var screenHeight = window.screen.height;
335 var windowWidth = Math.min(650, screenWidth * 0.8);
336 var windowHeight = Math.min(750, screenHeight * 0.8);
337 var left = (screenWidth - windowWidth) / 2;
338 var top = (screenHeight - windowHeight) / 2;
339
340 windowFeatures = 'width=' + windowWidth +
341 ',height=' + windowHeight +
342 ',left=' + left +
343 ',top=' + top +
344 ',scrollbars=yes,resizable=yes,toolbar=no,location=yes,status=yes';
345
346
347 }
348
349
350
351 // Open SSO URL in popup with enhanced window properties
352 saConnectWindow = window.open(
353 response.data.connect_url,
354 'searchatlas-connect',
355 windowFeatures
356 );
357
358
359
360 // Enhanced popup blocked detection
361 setTimeout(function () {
362 if (!saConnectWindow || saConnectWindow.closed || typeof saConnectWindow.closed === 'undefined') {
363 showConnectError('🚫 Popup Blocked',
364 'Your browser blocked the authentication popup. Please allow popups for this site and try again.',
365 [{
366 text: '🔄 Try Again',
367 action: function () {
368 handleSearchAtlasConnect();
369 }
370 }, {
371 text: '📝 How to Enable Popups',
372 action: function () {
373 showPopupHelp();
374 }
375 }, {
376 text: '🖥️ Open in New Tab',
377 action: function () {
378 window.open(response.data.connect_url, '_blank');
379 startSearchAtlasPolling(response.data.nonce_token);
380 }
381 }]
382 );
383 resetConnectButton();
384 return;
385 }
386
387 // Add focus to popup window
388 try {
389 saConnectWindow.focus();
390 } catch(e) {
391 // Ignore focus errors
392 }
393
394 // Update progress display and start polling
395 updateProgress(10, 1, 6); // Show initial progress
396 console.log('🔍 Starting connect polling...');
397 setTimeout(function () {
398 startSearchAtlasPolling(response.data.nonce_token);
399 }, 200);
400
401 }, 100); // Small delay to let popup settle
402
403 }, 500); // 500ms delay for better UX
404
405 } else {
406 var errorMessage = response.data.message || 'Failed to generate connect URL';
407 showConnectError('❌ Connection Failed',
408 errorMessage,
409 [{
410 text: '🔄 Retry Connection',
411 action: function () {
412 handleSearchAtlasConnect();
413 }
414 }]
415 );
416 resetConnectButton();
417 }
418 },
419 error: function (xhr, status, error) {
420 console.error('🐛 DEBUG: AJAX error occurred:', {
421 xhr: xhr,
422 status: status,
423 error: error,
424 responseText: xhr.responseText,
425 responseJSON: xhr.responseJSON,
426 readyState: xhr.readyState,
427 ajaxUrl: ajaxUrl
428 });
429
430 var errorMessage = 'Network error occurred while connecting to ' + getPluginName();
431
432 // Provide specific error messages based on the error type
433 if (status === 'timeout') {
434 errorMessage = 'Request timed out. Please check your internet connection and try again.';
435 } else if (status === 'error') {
436 if (xhr.status === 0) {
437 errorMessage = 'Unable to connect. Please check if WordPress admin-ajax.php is accessible.';
438 } else if (xhr.status === 403) {
439 errorMessage = 'Access denied. Please refresh the page and try again.';
440 } else if (xhr.status === 500) {
441 errorMessage = 'Server error occurred. Please check server logs for details.';
442 } else {
443 errorMessage = 'HTTP Error ' + xhr.status + ': ' + xhr.statusText;
444 }
445 } else if (xhr.responseJSON && xhr.responseJSON.message) {
446 errorMessage = xhr.responseJSON.message;
447 }
448
449 showConnectError('🌐 Network Error', errorMessage,
450 [{
451 text: '🔄 Retry Connection',
452 action: function () {
453 handleSearchAtlasConnect();
454 }
455 }, {
456 text: '🔧 Check Network',
457 action: function () {
458 console.log('Network diagnostics:', {
459 status: status,
460 error: error,
461 xhr: xhr
462 });
463 }
464 }]
465 );
466 resetConnectButton();
467 }
468 });
469 }
470
471 function resetConnectButton() {
472 var $button = $('#connect-searchatlas-btn');
473 var $progressContainer = $('.metasync-sa-connect-progress');
474 var hasApiKey = $('#searchatlas-api-key').val().trim() !== '';
475
476 $button.prop('disabled', false)
477 .removeClass('connecting authenticating success dashboard-loading') // Remove all loading classes
478 .html(hasApiKey ? '🔄 Re-authenticate with ' + getPluginName() : '🔗 Connect to ' + getPluginName());
479 $progressContainer.hide();
480 }
481
482 function startSearchAtlasPolling(nonceToken) {
483 var pollCount = 0;
484 var maxPolls = 12; // Poll for 60 seconds (12 * 5 seconds)
485 var $progressContainer = $('.metasync-sa-connect-progress');
486 var $progressFill = $('.metasync-sa-connect-progress-fill');
487 var $progressText = $('.metasync-sa-connect-progress-text');
488 var $button = $('#connect-searchatlas-btn');
489
490 // Progress display should already be initialized, just update it
491 updateProgress(0, 0, maxPolls);
492
493
494 saConnectPollingInterval = setInterval(function () {
495 pollCount++;
496
497
498 // Update progress bar
499 var progress = Math.min((pollCount / maxPolls) * 100, 100);
500 updateProgress(progress, pollCount, maxPolls);
501
502 // Check if window was closed manually - but continue polling
503 if (saConnectWindow && saConnectWindow.closed) {
504 // Popup closed, but continue polling to check for authentication success
505 console.log('🔍 Connect popup closed, continuing to poll for authentication success...');
506 saConnectWindow = null; // Clear reference to closed window
507
508 // Update UI to show we're still checking
509 updateProgress(75, pollCount, maxPolls);
510 $progressText.text('Popup closed - checking authentication status...');
511
512 // Continue polling - don't return, let the polling continue
513 }
514
515 // Stop polling after max attempts
516 if (pollCount >= maxPolls) {
517 stopSearchAtlasPolling();
518 if (saConnectWindow) {
519 saConnectWindow.close();
520 }
521
522 // �
523 Reset the authentication flow while keeping the timeout component
524 resetConnectButton();
525
526 showConnectError('⏰ Authentication Timeout',
527 'The authentication process timed out after 60 seconds. Please complete the authentication more quickly or check for network issues.',
528 [{
529 text: '🔄 Try Again',
530 action: function () {
531 handleSearchAtlasConnect();
532 }
533 }, {
534 text: '💬 Contact Support',
535 action: function () {
536 var supportEmail = metaSync.support_email || 'support@searchatlas.com';
537 window.open('mailto:' + supportEmail + '?subject=Connect Authentication Timeout (30s)', '_blank');
538 }
539 }]
540 );
541 return;
542 }
543
544 // Update button state periodically
545 if (pollCount % 2 === 0) { // Every 10 seconds
546 var timeLeft = Math.ceil((maxPolls - pollCount) * 5);
547 $button.html('<span class="metasync-sa-connect-loading"></span> Waiting for Authentication (' + timeLeft + 's left)');
548 }
549
550 // Check if API key was updated
551 $.ajax({
552 url: ajaxurl,
553 type: 'POST',
554 data: {
555 action: 'metasync_check_connect_status',
556 nonce: metaSync.sa_connect_nonce || '',
557 nonce_token: nonceToken
558 },
559 success: function (response) {
560 if (response.success && response.data.updated) {
561 stopSearchAtlasPolling();
562 if (saConnectWindow) {
563 saConnectWindow.close();
564 }
565
566 // Show completion animation
567 updateProgress(100, maxPolls, maxPolls);
568
569 var statusCode = response.data.status_code || 200;
570
571 // Handle different status codes with enhanced UX
572 if (statusCode === 200) {
573 // Success: Update all UI elements to reflect connected state
574 updateUIForConnectedState(response.data.api_key, response.data.otto_pixel_uuid);
575
576 // Refresh Plugin Auth Token field to show the auto-generated token
577 // This will either show the existing token or the newly auto-generated one
578 $.ajax({
579 url: ajaxurl,
580 type: 'POST',
581 data: {
582 action: 'metasync_get_plugin_auth_token',
583 nonce: metaSync.sa_connect_nonce || ''
584 },
585 success: function (tokenResponse) {
586 if (tokenResponse.success && tokenResponse.data.plugin_auth_token) {
587 $('#apikey').val(tokenResponse.data.plugin_auth_token);
588 console.log('🔑 Plugin Auth Token field updated after connect success');
589 }
590 },
591 error: function () {
592 console.log('⚠️ Could not refresh Plugin Auth Token field, but connect authentication was successful');
593 }
594 });
595
596 $button.removeClass('connecting authenticating dashboard-loading')
597 .addClass('success no-loading')
598 .html('�
599 Authentication Complete!');
600
601 // Add success animation to container
602 $button.closest('.metasync-sa-connect-container').addClass('metasync-sa-connect-success-animation');
603 setTimeout(function () {
604 $button.closest('.metasync-sa-connect-container').removeClass('metasync-sa-connect-success-animation');
605 }, 600);
606
607 // Track 1-click activation in GA4
608 var hasExistingApiKey = $('#searchatlas-api-key').val() && $('#searchatlas-api-key').val().trim() !== '';
609 $.ajax({
610 url: metaSync.ajax_url,
611 type: 'POST',
612 data: {
613 action: 'metasync_track_one_click_activation',
614 auth_method: 'searchatlas_connect',
615 is_reconnection: hasExistingApiKey
616 }
617 });
618 if (typeof window.metasyncGA4Track === 'function') {
619 window.metasyncGA4Track('one_click_activation', {
620 auth_method: 'searchatlas_connect',
621 is_reconnection: hasExistingApiKey
622 });
623 }
624
625 showConnectSuccess('🎉 Authentication Successful',
626 'Your ' + getPluginName() + ' account has been synced successfully! The page will reload to apply your new settings.',
627 [{
628 text: '🔄 Reload Now',
629 action: function () {
630 location.reload();
631 }
632 }]
633 );
634
635 // Auto-reload with countdown
636 var countdown = 3;
637 var countdownInterval = setInterval(function () {
638 countdown--;
639 if (countdown > 0) {
640 $button.html('�
641 Reloading in ' + countdown + '...');
642 } else {
643 clearInterval(countdownInterval);
644 location.reload();
645 }
646 }, 1000);
647
648 } else if (statusCode === 404) {
649 // Website not registered
650 var effectiveDomain = response.data.effective_domain || metaSync.dashboard_domain;
651 showConnectNotRegistered(effectiveDomain);
652 resetConnectButton();
653
654 } else if (statusCode === 500) {
655 // Server error
656 showConnectError('🔧 Server Error',
657 'A server error occurred during authentication. This is usually temporary.',
658 [{
659 text: '🔄 Try Again',
660 action: function () {
661 handleSearchAtlasConnect();
662 }
663 }, {
664 text: '💬 Contact Support',
665 action: function () {
666 var supportEmail = metaSync.support_email || 'support@searchatlas.com';
667 window.open('mailto:' + supportEmail + '?subject=Connect Server Error (Code 500)', '_blank');
668 }
669 }]
670 );
671 resetConnectButton();
672
673 } else {
674 // Unknown status
675 showConnectError('❓ Unexpected Status',
676 'Received an unexpected status code (' + statusCode + ') during authentication.',
677 [{
678 text: '🔄 Try Again',
679 action: function () {
680 handleSearchAtlasConnect();
681 }
682 }]
683 );
684 resetConnectButton();
685 }
686 }
687 },
688 error: function (xhr, status, error) {
689 // Continue polling even if individual request fails, but provide feedback
690 if (pollCount % 6 === 0) { // Every 30 seconds, show a subtle warning
691 console.log('Connect polling request failed, continuing... Error:', error);
692 // Don't show error to user for temporary network issues during polling
693 }
694
695 }
696 });
697 }, 5000); // Poll every 5 seconds
698 }
699
700 function initializeProgressDisplay() {
701 var $progressContainer = $('.metasync-sa-connect-progress');
702 var $button = $('#connect-searchatlas-btn');
703
704 // Hide any existing status messages to avoid duplication
705 hideConnectStatus();
706
707 // Create progress elements if they don't exist
708 if ($progressContainer.length === 0) {
709 var progressHTML = `
710 <div class="metasync-sa-connect-progress">
711 <div class="metasync-sa-connect-progress-header">
712 <strong>🔐 Authentication in Progress</strong>
713 <span class="metasync-sa-connect-progress-time">Connecting...</span>
714 </div>
715 <div class="metasync-sa-connect-progress-bar">
716 <div class="metasync-sa-connect-progress-fill"></div>
717 </div>
718 <div class="metasync-sa-connect-progress-text">
719 Establishing secure connection to ' + getPluginName() + '...
720 </div>
721 </div>
722 `;
723 $button.closest('.metasync-sa-connect-container').append(progressHTML);
724 $progressContainer = $('.metasync-sa-connect-progress');
725 }
726
727 $progressContainer.show().find('.metasync-sa-connect-progress-fill').css('width', '0%');
728 }
729
730 function updateProgress(percentage, currentPoll, maxPolls) {
731 var $progressFill = $('.metasync-sa-connect-progress-fill');
732 var $progressTime = $('.metasync-sa-connect-progress-time');
733 var $progressText = $('.metasync-sa-connect-progress-text');
734
735 // Update progress bar
736 $progressFill.css('width', percentage + '%');
737
738 // Update time display (now in seconds)
739 var timeElapsed = currentPoll * 5;
740 var timeRemaining = (maxPolls - currentPoll) * 5;
741 $progressTime.text(timeElapsed + 's elapsed, ' + timeRemaining + 's remaining');
742
743 // Update progress text based on time elapsed (optimized for 60-second timeout)
744 var progressMessages = [
745 'Establishing connection and opening authentication window...',
746 'Please complete authentication in the popup window...',
747 'Almost done! Finalizing your authentication...'
748 ];
749
750 var messageIndex = Math.floor((currentPoll / maxPolls) * progressMessages.length);
751 messageIndex = Math.min(messageIndex, progressMessages.length - 1);
752 $progressText.text(progressMessages[messageIndex]);
753 }
754
755 // Update the old function name for compatibility
756 function startConnectPolling(nonceToken) {
757 return startSearchAtlasPolling(nonceToken);
758 }
759
760 function stopSearchAtlasPolling() {
761 if (saConnectPollingInterval) {
762 clearInterval(saConnectPollingInterval);
763 saConnectPollingInterval = null;
764 }
765 }
766
767 // Legacy function for backward compatibility
768 function stopConnectPolling() {
769 return stopSearchAtlasPolling();
770 }
771
772 function showConnectSuccess(title, message, actions) {
773 showConnectStatus('success', title, message, actions);
774 }
775
776 function showConnectError(title, message, actions) {
777 showConnectStatus('error', title, message, actions);
778 }
779
780 function showConnectInfo(title, message, actions) {
781 showConnectStatus('info', title, message, actions);
782 }
783
784 function showConnectWarning(title, message, actions) {
785 showConnectStatus('warning', title, message, actions);
786 }
787
788 function showConnectStatus(type, title, message, actions) {
789 var $statusContainer = $('#sa-connect-status-message');
790 var $button = $('#connect-searchatlas-btn');
791
792 // Create enhanced status container if it doesn't exist
793 if ($statusContainer.length === 0 || !$statusContainer.hasClass('metasync-sa-connect-status')) {
794 // Create new enhanced status container
795 var statusHTML = '<div id="sa-connect-status-message" class="metasync-sa-connect-status"></div>';
796 $button.closest('.metasync-sa-connect-container').length === 0 ?
797 $button.parent().append(statusHTML) :
798 $button.closest('.metasync-sa-connect-container').append(statusHTML);
799 $statusContainer = $('#sa-connect-status-message');
800 }
801
802 // Build status content
803 var html = '<div class="metasync-sa-connect-status-content">';
804 html += '<div class="metasync-sa-connect-status-title">' + title + '</div>';
805 if (message) {
806 html += '<div class="metasync-sa-connect-status-message">' + message + '</div>';
807 }
808 html += '</div>';
809
810 // Add action buttons if provided
811 if (actions && actions.length > 0) {
812 html += '<div class="metasync-sa-connect-actions">';
813 actions.forEach(function (action, index) {
814 var buttonClass = action.primary ? 'primary' : 'secondary';
815 html += '<button type="button" class="metasync-sa-connect-btn ' + buttonClass + '" data-action="' + index + '">';
816 html += action.text;
817 html += '</button>';
818 });
819 html += '</div>';
820 }
821
822 // Update status container with animation
823 $statusContainer
824 .removeClass('success error info warning')
825 .addClass(type)
826 .html(html)
827 .hide()
828 .slideDown(300);
829
830 // Bind action handlers
831 if (actions && actions.length > 0) {
832 $statusContainer.find('.metasync-sa-connect-btn').off('click').on('click', function () {
833 var $actionBtn = $(this);
834 var actionIndex = parseInt($actionBtn.data('action'));
835 if (actions[actionIndex] && typeof actions[actionIndex].action === 'function') {
836 var originalText = $actionBtn.text();
837 $actionBtn.prop('disabled', true)
838 .addClass('no-loading') // Prevent dashboard.js conflicts
839 .removeClass('dashboard-loading')
840 .html('<span class="metasync-sa-connect-loading"></span> ' + originalText);
841 setTimeout(function () {
842 actions[actionIndex].action();
843 }, 100);
844 }
845 });
846 }
847
848 // Auto-scroll to status message for better visibility
849 if (type === 'error' || type === 'warning' || type === 'success') {
850 setTimeout(function () {
851 $('html, body').animate({
852 scrollTop: $statusContainer.offset().top - 100
853 }, 300);
854 }, 100);
855 }
856 }
857
858 function showConnectNotRegistered(dashboardDomain) {
859 // Use dashboard domain if provided, otherwise fallback to effective domain (includes whitelabel)
860 var domain = dashboardDomain || metaSync.dashboard_domain;
861 var registerUrl = domain + '/seo-automation-v3/create-project';
862
863 showConnectWarning(
864 '⚠️ Website Not Registered',
865 'Your website hasn\'t been registered with ' + getPluginName() + ' yet. Registration is required to enable 1-click connect to retrieve your Search Atlas API key and Otto UUID.',
866 [{
867 text: '🌐 Register Website',
868 action: function () {
869 window.open(registerUrl, '_blank');
870 },
871 primary: true
872 }, {
873 text: '📚 Learn More About Registration',
874 action: function () {
875 var docDomain = metaSync.documentation_domain || 'https://searchatlas.com';
876 window.open(docDomain, '_blank');
877 }
878 }, {
879 text: '🔄 Try Authentication Again',
880 action: function () {
881 setTimeout(function () {
882 handleSearchAtlasConnect();
883 }, 500);
884 }
885 }]
886 );
887 }
888
889 function hideConnectStatus() {
890 $('#sa-connect-status-message').slideUp(300);
891 $('.metasync-sa-connect-progress').slideUp(300);
892 }
893
894 function showPopupHelp() {
895 var helpContent = `
896 <div style="max-width: 500px;">
897 <h3>🔧 How to Enable Popups</h3>
898 <p><strong>Chrome/Edge:</strong></p>
899 <ol>
900 <li>Click the popup blocked icon in the address bar</li>
901 <li>Select "Always allow popups from this site"</li>
902 <li>Reload the page and try again</li>
903 </ol>
904 <p><strong>Firefox:</strong></p>
905 <ol>
906 <li>Click the shield icon in the address bar</li>
907 <li>Turn off "Block popup windows"</li>
908 <li>Refresh and try again</li>
909 </ol>
910 <p><strong>Safari:</strong></p>
911 <ol>
912 <li>Go to Safari → Preferences → Websites</li>
913 <li>Select "Pop-up Windows" on the left</li>
914 <li>Set this website to "Allow"</li>
915 </ol>
916 </div>
917 `;
918
919 showConnectInfo('📝 Popup Help', helpContent, [{
920 text: '�
921 Got it, Try Again',
922 action: function () {
923 handleSearchAtlasConnect();
924 },
925 primary: true
926 }]);
927 }
928
929 function enhancedErrorRecovery(error, context) {
930 console.group('🔍 Connect Error Diagnostics');
931 console.log('Error Context:', context);
932 console.log('Error Details:', error);
933 console.log('Browser Info:', {
934 userAgent: navigator.userAgent,
935 cookieEnabled: navigator.cookieEnabled,
936 language: navigator.language,
937 platform: navigator.platform
938 });
939 console.log('Current Time:', new Date().toISOString());
940 console.groupEnd();
941
942 // Provide contextual recovery suggestions
943 var recoverySuggestions = [];
944
945 if (context === 'network') {
946 recoverySuggestions = [
947 'Check your internet connection',
948 'Disable VPN or proxy if enabled',
949 'Try refreshing the page',
950 'Clear browser cache and cookies'
951 ];
952 } else if (context === 'popup') {
953 recoverySuggestions = [
954 'Allow popups for this website',
955 'Disable ad blockers temporarily',
956 'Try using a different browser',
957 'Check if firewall is blocking the request'
958 ];
959 } else if (context === 'timeout') {
960 recoverySuggestions = [
961 'Complete authentication within 60 seconds',
962 'Check if the popup window needs attention',
963 'Ensure you have your ' + getPluginName() + ' login ready',
964 'Try the authentication process again',
965 'Contact support if timeouts persist'
966 ];
967 }
968
969 return recoverySuggestions;
970 }
971
972 // Add enhanced page visibility handling
973 function handlePageVisibilityChange() {
974 if (document.hidden && saConnectWindow && !saConnectWindow.closed) {
975 // Page became hidden while SSO is in progress
976 showConnectInfo('👁️ Page Hidden',
977 'This page is now in the background. The authentication will continue, but you may want to return to this tab to see the results.');
978 }
979 }
980
981 // Initialize enhanced features when document is ready
982 $(document).ready(function () {
983
984 // Hide Jetpack identity crisis container on plugin pages
985 if ($('.metasync-dashboard-wrap').length > 0) {
986 $('#jp-identity-crisis-container, .jp-identity-crisis-container').hide();
987 }
988
989 // Check for URL parameters and show success/error messages
990 const urlParams = new URLSearchParams(window.location.search);
991
992 // Show success message for cleared logs
993 if (urlParams.get('log_cleared') === '1') {
994 showSyncSuccess('🧹 Error Logs Cleared', 'All error log entries have been successfully cleared.');
995 }
996
997 // Show success message for cleared error summary
998 if (urlParams.get('error_summary_cleared') === '1') {
999 showSyncSuccess('📊 Error Summary Cleared', 'Error summary has been cleared successfully.');
1000 }
1001
1002 // Show error message for failed clear operation
1003 if (urlParams.get('clear_error') === '1') {
1004 showSyncError('❌ Clear Failed', 'Unable to clear the error logs. Please check permissions or try again.');
1005 }
1006
1007 // Show error message for failed error summary clear
1008 if (urlParams.get('error_summary_error') === '1') {
1009 showSyncError('❌ Clear Failed', 'Unable to clear the error summary. Please try again.');
1010 }
1011
1012 // Check global variables are available
1013
1014 // Test AJAX connectivity using our specific endpoint
1015 if (typeof ajaxurl !== 'undefined' && ajaxurl) {
1016 $.ajax({
1017 url: ajaxurl,
1018 type: 'POST',
1019 data: {
1020 action: 'metasync_test_ajax_endpoint',
1021 nonce: metaSync.sa_connect_nonce
1022 },
1023 timeout: 10000,
1024 success: function (response) {
1025 if (!response.success) {
1026 console.warn('🐛 DEBUG: AJAX endpoint reached but returned success=false:', response.data);
1027 }
1028 },
1029 error: function (xhr, status, error) {
1030 console.error('🐛 DEBUG: AJAX test failed:', {
1031 xhr: xhr,
1032 status: status,
1033 error: error,
1034 responseText: xhr.responseText,
1035 ajaxurl: ajaxurl
1036 });
1037
1038 // Try alternative AJAX test
1039 $.post(ajaxurl, {
1040 action: 'wp_ajax_nopriv_heartbeat'
1041 }).done(function (response2) {
1042 }).fail(function (xhr2) {
1043 console.error('🐛 DEBUG: Alternative AJAX also failed:', xhr2);
1044 });
1045 }
1046 });
1047 }
1048
1049 // Check if SSO button exists and is functional
1050 var $connectButton = $('#connect-searchatlas-btn');
1051
1052 // Test direct click event binding and fix dashboard interference
1053 if ($connectButton.length > 0) {
1054 // Aggressively prevent dashboard loading interference
1055 preventDashboardInterference($connectButton);
1056
1057 $connectButton.off('click').on('click', function (e) {
1058
1059 // Prevent dashboard.js from interfering
1060 preventDashboardInterference($(this));
1061
1062 // Call handleSearchAtlasConnect if not already disabled by another process
1063 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1064 handleSearchAtlasConnect();
1065 } else {
1066 }
1067 });
1068 }
1069
1070 // Add page visibility change handler
1071 if (typeof document.hidden !== 'undefined') {
1072 document.addEventListener('visibilitychange', handlePageVisibilityChange);
1073 }
1074
1075 // Add keyboard shortcuts for better accessibility
1076 $(document).on('keydown', function (e) {
1077 // Escape key to cancel ongoing SSO process
1078 if (e.key === 'Escape' && saConnectPollingInterval) {
1079 if (confirm('Cancel the ongoing authentication process?')) {
1080 stopSearchAtlasPolling();
1081 if (saConnectWindow) {
1082 saConnectWindow.close();
1083 }
1084 showConnectInfo('⏸️ Authentication Cancelled', 'You cancelled the authentication process.');
1085 resetConnectButton();
1086 }
1087 }
1088 });
1089
1090 // Add connection status indicator
1091 function updateConnectionStatus() {
1092 var $button = $('#connect-searchatlas-btn');
1093 var $apiKeyField = $('#searchatlas-api-key');
1094
1095 // Only update status if we're on a page with the API key field (General Settings)
1096 // On other pages, preserve the PHP-determined status in the header
1097 if ($apiKeyField.length === 0) {
1098 return; // Don't update status on pages without the API key field
1099 }
1100
1101 var hasApiKey = $apiKeyField.val() && $apiKeyField.val().trim() !== '';
1102 var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
1103 var isFullyConnected = hasApiKey && hasOttoUuid;
1104
1105 // Update button text based on connection state
1106 if (!$button.prop('disabled')) {
1107 if (isFullyConnected) {
1108 $button.html('🔄 Re-authenticate with ' + getPluginName());
1109 } else if (hasApiKey && !hasOttoUuid) {
1110 $button.html('🔧 Complete Authentication Setup');
1111 } else {
1112 $button.html('🔗 Connect to ' + getPluginName());
1113 }
1114 }
1115
1116 // Update header status indicator (only on General Settings page)
1117 if (isFullyConnected) {
1118 updateHeaderStatus(true, 'Synced', getPluginName() + ' API key and ' + getOttoName() + ' UUID are configured');
1119 } else if (hasApiKey && !hasOttoUuid) {
1120 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1121 } else {
1122 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1123 }
1124 }
1125
1126 // Monitor API key field changes
1127 $('#searchatlas-api-key').on('input', updateConnectionStatus);
1128
1129 // Initial status update
1130 updateConnectionStatus();
1131
1132 // Initialize dashboard iframe functionality
1133 initializeDashboardIframe();
1134
1135 // Settings dropdown now handled by inline script in HTML
1136
1137 // Add debug function for connection status (accessible in console)
1138 window.debugConnectionStatus = function () {
1139 var apiKey = $('#searchatlas-api-key').val();
1140 var hasApiKey = apiKey.trim() !== '';
1141
1142 console.log('🔍 Connection Status Debug:', {
1143 searchatlas_api_key: hasApiKey ? (apiKey.substring(0, 8) + '...') : 'EMPTY',
1144 otto_pixel_uuid: metaSync.otto_pixel_uuid || 'NOT SET',
1145 connection_state: hasApiKey && metaSync.otto_pixel_uuid ? 'CONNECTED' :
1146 hasApiKey ? 'PARTIAL (Missing ' + getOttoName() + ' UUID)' : 'NOT CONNECTED',
1147 dashboard_tab_visible: hasApiKey && metaSync.otto_pixel_uuid ? 'YES' : 'NO',
1148 status_indicator_should_show: hasApiKey && metaSync.otto_pixel_uuid ? 'Synced' : 'Not Synced'
1149 });
1150 };
1151
1152
1153 });
1154
1155 // Settings dropdown is now handled by inline script in HTML for better reliability
1156
1157 /**
1158 * Initialize Dashboard Iframe functionality
1159 * Adds loading states and error handling for the embedded dashboard
1160 */
1161 function initializeDashboardIframe() {
1162 var $iframe = $('#metasync-dashboard-iframe');
1163
1164 if ($iframe.length === 0) {
1165 return; // No iframe on this page
1166 }
1167
1168 // Add loading indicator
1169 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1170 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Loading dashboard...</p></div>';
1171 $wrapper.append(loadingHTML);
1172
1173 // Handle iframe load events
1174 $iframe.on('load', function () {
1175 $('.metasync-dashboard-iframe-loading').fadeOut(300);
1176
1177 // Log successful load
1178 console.log('Dashboard iframe loaded successfully');
1179 });
1180
1181 // Handle iframe error events
1182 $iframe.on('error', function () {
1183 $('.metasync-dashboard-iframe-loading').html(
1184 '<div style="text-align: center; color: #dc3232;">' +
1185 '<h3>❌ Dashboard Loading Error</h3>' +
1186 '<p>Unable to load the dashboard. Please check your connection.</p>' +
1187 '<button type="button" class="button button-primary" onclick="location.reload();">🔄 Reload Page</button>' +
1188 '</div>'
1189 );
1190
1191 console.error('Dashboard iframe failed to load');
1192 });
1193
1194 // Add keyboard shortcut for refreshing iframe
1195 $(document).on('keydown', function (e) {
1196 // Ctrl/Cmd + R on dashboard page refreshes iframe
1197 if ((e.ctrlKey || e.metaKey) && e.key === 'r' && $iframe.length > 0) {
1198 e.preventDefault();
1199 refreshDashboardIframe();
1200 }
1201 });
1202
1203 // Handle iframe resize for better mobile experience
1204 function adjustIframeHeight() {
1205 if (window.innerWidth <= 768) {
1206 $iframe.height(600);
1207 } else {
1208 $iframe.height(800);
1209 }
1210 }
1211
1212 // Adjust on window resize
1213 $(window).on('resize', adjustIframeHeight);
1214 adjustIframeHeight(); // Initial adjustment
1215 }
1216
1217 /**
1218 * Refresh Dashboard Iframe
1219 * Reloads the iframe content with loading indicator
1220 */
1221 function refreshDashboardIframe() {
1222 var $iframe = $('#metasync-dashboard-iframe');
1223 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1224
1225 if ($iframe.length === 0) {
1226 return;
1227 }
1228
1229 // Show loading indicator
1230 $('.metasync-dashboard-iframe-loading').remove();
1231 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Refreshing dashboard...</p></div>';
1232 $wrapper.append(loadingHTML);
1233
1234 // Refresh iframe
1235 var currentSrc = $iframe.attr('src');
1236 $iframe.attr('src', '');
1237 setTimeout(function () {
1238 $iframe.attr('src', currentSrc);
1239 }, 100);
1240
1241 console.log('Dashboard iframe refresh initiated');
1242 }
1243
1244 /**
1245 * Handle Search Atlas Authentication Reset.
1246 * Shows confirmation dialog and clears the Search Atlas API key and Otto UUID.
1247 */
1248 function handleSearchAtlasResetAuth() {
1249 // Show confirmation dialog
1250 var confirmed = confirm(
1251 '⚠️ Disconnect ' + getPluginName() + ' Account\n\n' +
1252 'This will:\n' +
1253 '• Remove your ' + getPluginName() + ' API key\n' +
1254 '• Clear all authentication tokens\n' +
1255 '• Reset connection timestamps\n' +
1256 '• Clear cached authentication data\n\n' +
1257 'You will need to re-authenticate to use ' + getPluginName() + ' features.\n\n' +
1258 'Are you sure you want to continue?'
1259 );
1260
1261 if (!confirmed) {
1262 return;
1263 }
1264
1265 var $resetButton = $('#reset-searchatlas-auth');
1266 var $connectButton = $('#connect-searchatlas-btn');
1267 var $apiKeyField = $('#searchatlas-api-key');
1268
1269 // Show loading state (prevent dashboard.js conflicts)
1270 $resetButton.prop('disabled', true)
1271 .addClass('no-loading') // Prevent dashboard.js interference
1272 .removeClass('dashboard-loading')
1273 .html('<span class="metasync-sa-connect-loading"></span> Disconnecting...');
1274
1275 // Show status message
1276 showConnectInfo('🔄 Disconnecting', 'Clearing your ' + getPluginName() + ' authentication data...');
1277
1278 // Make AJAX call to reset authentication
1279 $.ajax({
1280 url: ajaxurl,
1281 type: 'POST',
1282 data: {
1283 action: 'metasync_reset_authentication',
1284 nonce: metaSync.reset_auth_nonce
1285 },
1286 success: function (response) {
1287 if (response.success) {
1288 // Clear the API key field
1289 $apiKeyField.val('');
1290
1291 // Update button states
1292 $connectButton.html('🔗 Connect to ' + getPluginName());
1293 $resetButton.remove(); // Remove reset button since no longer connected
1294
1295 // Show clean success message without duplicate connect functionality
1296 showConnectSuccess('�
1297 Account Disconnected',
1298 'Your ' + getPluginName() + ' authentication has been completely reset. All authentication data has been cleared.',
1299 [{
1300 text: '📄 View What Was Cleared',
1301 action: function () {
1302 showClearedDataDetails(response.data.cleared_data);
1303 },
1304 primary: true
1305 }, {
1306 text: '�
1307 Got it',
1308 action: function () {
1309 hideConnectStatus();
1310 }
1311 }]
1312 );
1313
1314 // Update page elements to reflect disconnected state
1315 updateUIForDisconnectedState();
1316
1317 } else {
1318 showConnectError('❌ Reset Failed',
1319 response.data.message || 'Failed to reset authentication',
1320 [{
1321 text: '🔄 Try Again',
1322 action: function () {
1323 handleSearchAtlasResetAuth();
1324 }
1325 }, {
1326 text: '💬 Contact Support',
1327 action: function () {
1328 var supportEmail = metaSync.support_email || 'support@searchatlas.com';
1329 window.open('mailto:' + supportEmail + '?subject=Authentication Reset Failed', '_blank');
1330 }
1331 }]
1332 );
1333 }
1334 },
1335 error: function (xhr, status, error) {
1336 showConnectError('🌐 Network Error',
1337 'A network error occurred while trying to reset authentication.',
1338 [{
1339 text: '🔄 Try Again',
1340 action: function () {
1341 handleSearchAtlasResetAuth();
1342 }
1343 }]
1344 );
1345 },
1346 complete: function () {
1347 // Reset button state
1348 $resetButton.prop('disabled', false)
1349 .removeClass('dashboard-loading no-loading')
1350 .html('🔓 Disconnect Account');
1351 }
1352 });
1353 }
1354
1355 /**
1356 * Show details of what data was cleared during reset
1357 */
1358 function showClearedDataDetails(clearedData) {
1359 var details = '<div style="max-width: 500px;"><h3>🗑️ Data Cleared</h3><ul style="text-align: left; margin: 15px 0;">';
1360
1361 for (var key in clearedData) {
1362 var displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
1363 details += '<li><strong>' + displayName + ':</strong> ' + clearedData[key] + '</li>';
1364 }
1365
1366 details += '</ul><p style="color: #666; font-size: 13px;">This data has been permanently removed. You can safely reconnect with a new or existing ' + getPluginName() + ' account.</p></div>';
1367
1368 showConnectInfo('📋 Reset Details', details, [{
1369 text: '�
1370 Got it',
1371 action: function () {
1372 hideConnectStatus();
1373 // Highlight the main connect button briefly to guide user attention
1374 var $mainButton = $('#connect-searchatlas-btn');
1375 if ($mainButton.length > 0) {
1376 $mainButton.addClass('metasync-pulse');
1377 setTimeout(function () {
1378 $mainButton.removeClass('metasync-pulse');
1379 }, 2000);
1380 }
1381 },
1382 primary: true
1383 }]);
1384 }
1385
1386 /**
1387 * Update UI elements when account is disconnected
1388 */
1389 function updateUIForDisconnectedState() {
1390 // Update API key field placeholder
1391 $('#searchatlas-api-key').attr('placeholder', 'Your API key will appear here after authentication');
1392
1393 // �
1394 Clear OTTO Pixel UUID field
1395 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val('');
1396
1397 // Note: OTTO SSR is always enabled by default, no checkbox to uncheck
1398
1399 // Remove synced indicator from API key field
1400 $('.metasync-sa-connect-container').find('span:contains("✓ Synced")').remove();
1401 $('label[for="searchatlas-api-key"]').find('span').remove(); // Remove any status spans
1402
1403 // Update header status indicator to "Not Synced"
1404 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1405
1406 // Update metaSync object for JavaScript state tracking
1407 if (typeof metaSync !== 'undefined') {
1408 metaSync.searchatlas_api_key = false;
1409 metaSync.otto_pixel_uuid = '';
1410 metaSync.is_connected = false;
1411 }
1412
1413 // Update descriptions to reflect disconnected state
1414 $('.metasync-sa-connect-description').html(
1415 'Connect your ' + getPluginName() + ' account with one click. This will automatically configure your API key below and enable all plugin features.'
1416 );
1417
1418 // Clear timestamp display if it exists
1419 $('#sendAuthTokenTimestamp').fadeOut(300);
1420
1421 // Header status already updated above - no need for additional connection status call
1422
1423 console.log('🔄 UI updated to reflect disconnected state - cleared API key, OTTO UUID, and OTTO enable checkbox');
1424
1425 // Show clean success message without duplicate connect button
1426 setTimeout(function () {
1427 showConnectSuccess('�
1428 Account Disconnected',
1429 'Your ' + getPluginName() + ' authentication has been completely reset. Use the "Connect to ' + getPluginName() + '" button above to reconnect.',
1430 [{
1431 text: '�
1432 Got it',
1433 action: function () {
1434 hideConnectStatus();
1435 },
1436 primary: true
1437 }]
1438 );
1439 }, 500); // Shorter delay since no action is needed
1440 }
1441
1442 /**
1443 * Update UI elements when account is connected/authenticated
1444 * Complementary function to updateUIForDisconnectedState()
1445 */
1446 function updateUIForConnectedState(apiKey, ottoPixelUuid) {
1447 // Update API key field
1448 if (apiKey) {
1449 $('#searchatlas-api-key').val(apiKey);
1450 }
1451
1452 // �
1453 Update OTTO Pixel UUID field
1454 if (ottoPixelUuid) {
1455 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val(ottoPixelUuid);
1456 }
1457
1458 // Note: OTTO SSR is always enabled by default, no checkbox needed
1459
1460 // Update header status indicator based on UUID presence
1461 if (ottoPixelUuid) {
1462 updateHeaderStatus(true, 'Synced', 'Authentication completed - heartbeat sync will be validated on next page load');
1463 } else {
1464 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1465 }
1466
1467 // Update metaSync object for JavaScript state tracking
1468 if (typeof metaSync !== 'undefined') {
1469 metaSync.searchatlas_api_key = true;
1470 metaSync.is_connected = true;
1471 if (ottoPixelUuid) {
1472 metaSync.otto_pixel_uuid = ottoPixelUuid;
1473 }
1474 }
1475
1476 // Update descriptions to reflect connected state
1477 $('.metasync-sa-connect-description').html(
1478 'Your ' + getPluginName() + ' account is connected and synced successfully. All plugin features are now enabled.'
1479 );
1480
1481 console.log('�
1482 UI updated to reflect connected state - API key set, OTTO UUID set (SSR always enabled)');
1483 }
1484
1485 function addClassTableRowLocalSEO() {
1486 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1487 const myElement = document.getElementsByTagName('tr');
1488
1489 for (let i = 0; i < myElement.length; i++) {
1490 myElement[i].classList.add('metasync-seo-' + (i + 10));
1491 }
1492 }
1493 }
1494
1495 function addClassTableRowSiteInfo() {
1496 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1497 const myElement = document.getElementsByTagName('tr');
1498
1499 for (let i = 0; i < myElement.length; i++) {
1500 myElement[i].classList.add('metasync-site-info-' + (i + 10));
1501 }
1502 }
1503 }
1504
1505 function uploadMedia(title, text, input, src, closeBtn) {
1506
1507 var mediaUploader;
1508
1509 // If the uploader object has already been created, reopen the dialog
1510 if (mediaUploader) {
1511 mediaUploader.open();
1512 return;
1513 }
1514 // Extend the wp.media object
1515 mediaUploader = wp.media.frames.file_frame = wp.media({
1516 title: title,
1517 button: {
1518 text: text
1519 }, multiple: false
1520 });
1521
1522 // When a file is selected, grab the URL and set it as the text field's value
1523 mediaUploader.on('select', function () {
1524 var attachment = mediaUploader.state().get('selection').first().toJSON();
1525 jQuery('#' + input).val(attachment.id);
1526 jQuery('#' + src).attr('src', attachment.url);
1527 jQuery('#' + src).attr('width', 300);
1528 jQuery('#' + closeBtn).attr('type', 'button');
1529 jQuery('#' + src).show();
1530 jQuery('#' + closeBtn).show();
1531 });
1532 // Open the uploader dialog
1533 mediaUploader.open();
1534 }
1535
1536 function getLocalSeoOnLoadPage() {
1537 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1538 var $type = $('#local_seo_person_organization').val();
1539 const classes = ['17', '18', '19', '20', '21', '24', '25'];
1540 if ($type === 'Person') {
1541 for (let i = 0; i < classes.length; i++) {
1542 $('.metasync-seo-' + classes[i]).hide();
1543 }
1544 $('.metasync-seo-15').show();
1545 } else {
1546 for (let i = 0; i < classes.length; i++) {
1547 $('.metasync-seo-' + classes[i]).show();
1548 }
1549 $('.metasync-seo-15').hide();
1550 }
1551 }
1552 }
1553
1554 function siteInfoOnLoadPage() {
1555 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1556 var $type = $('#site_info_type').val();
1557 const classes = ['18', '19'];
1558 if ($type === 'blog' || $type === 'portfolio' || $type === 'otherpersonal') {
1559 for (let i = 0; i < classes.length; i++) {
1560 $('.metasync-site-info-' + classes[i]).hide();
1561 }
1562 } else {
1563 for (let i = 0; i < classes.length; i++) {
1564 $('.metasync-site-info-' + classes[i]).show();
1565 }
1566 }
1567 }
1568 }
1569
1570 function deleteTime() {
1571 $(this).parent().remove();
1572 }
1573
1574 function hideElementById(id) {
1575 if ($('#' + id)) {
1576 $('#' + id).hide();
1577 }
1578 }
1579
1580 function removeValueById(id) {
1581 if ($('#' + id)) {
1582 $('#' + id).val('');
1583 }
1584 }
1585
1586 $(function () {
1587 $('#addNewTime').on('click', function () {
1588 $('#daysTime').append(
1589 '<li>' +
1590 '<select name="metasync_options[localseo][days][]">' +
1591 '<option value="Monday">Monday</option>' +
1592 '<option value="Tuseday">Tuseday</option>' +
1593 '<option value="Wednesday">Wednesday</option>' +
1594 '<option value="Thursday">Thursday</option>' +
1595 '<option value="Friday">Friday</option>' +
1596 '<option value="Saturday">Saturday</option>' +
1597 '<option value="Sunday">Sunday</option>' +
1598 '</select>' +
1599 '<input type="text" name="metasync_options[localseo][times][]">' +
1600 '<button id="timeDelete">Delete</button>' +
1601 '</li>');
1602 return;
1603 });
1604 $(document).on('click', '#timeDelete', deleteTime);
1605 });
1606
1607 function deleteNumber() {
1608 $(this).parent().remove();
1609 }
1610
1611 $(function () {
1612 $('#addNewNumber').on('click', function () {
1613 $('#phone-numbers').append(
1614 '<li>' +
1615 '<select name="metasync_options[localseo][phonetype][]">' +
1616 '<option value="Customer Service">Customer Service</option>' +
1617 '<option value="Technical Support">Technical Support</option>' +
1618 '<option value="Billing Support">Billing Support</option>' +
1619 '<option value="Bill Payment">Bill Payment</option>' +
1620 '<option value="Sales">Sales</option>' +
1621 '<option value="Reservations">Reservations</option>' +
1622 '<option value="Credit Card Support">Credit Card Support</option>' +
1623 '<option value="Emergency">Emergency</option>' +
1624 '<option value="Baggage Tracking">Baggage Tracking</option>' +
1625 '<option value="Roadside Assistance">Roadside Assistance</option>' +
1626 '<option value="Package Tracking">Package Tracking</option>' +
1627 '</select>' +
1628 '<input type="text" name="metasync_options[localseo][phonenumber][]">' +
1629 '<button id="number-delete">Delete</button>' +
1630 '</li>');
1631 return;
1632 });
1633 $(document).on('click', '#number-delete', deleteNumber);
1634 });
1635
1636 function deleteSourceUrl() {
1637 $(this).parent().remove();
1638 }
1639 $(function () {
1640 $('#addNewSourceUrl').on('click', function () {
1641 $('#source_urls').append(
1642 '<li>' +
1643 '<input type="text" class="regular-text" name="source_url[]">' +
1644 '<select name="search_type[]">' +
1645 '<option value="exact">Exact</option>' +
1646 '<option value="contain">Contain</option>' +
1647 '<option value="start">Start With</option>' +
1648 '<option value="end">End With</option>' +
1649 '</select>' +
1650 '<button id="source_url_delete">Remove</button>' +
1651 '</li>');
1652 return;
1653 });
1654 $(document).on('click', '#source_url_delete', deleteSourceUrl);
1655 });
1656
1657 $(function () {
1658
1659 setToken();
1660
1661 $('body').on('click', '#wp_metasync_sync', function (e) {
1662 e.preventDefault();
1663 metasync_syncPostsAndPages();
1664 });
1665 $('body').on('click', '#metasync_settings_genkey_btn', function () {
1666 $('#apikey').val(metasyncGenerateAPIKey());
1667 });
1668 $('body').on('click', '#lgloginbtn', function () {
1669 // Hide any existing error messages first
1670 $('#lgerror').addClass('hidden').hide();
1671
1672 if ($('#lgusername').val() === '' || $('#lgpassword').val() === '') {
1673 $('.input.lguser').toggleClass('hidden');
1674 } else {
1675 metasyncLGLogin($('#lgusername').val(), $('#lgpassword').val());
1676 }
1677 });
1678
1679 // Enhanced SSO Connect button event handler
1680 // Aggressive event binding that overrides dashboard.js interference
1681 $('body').off('click', '#connect-searchatlas-btn').on('click', '#connect-searchatlas-btn', function (e) {
1682
1683 // Aggressively prevent dashboard interference
1684 preventDashboardInterference($(this));
1685
1686 // Only proceed if button is not in SSO process
1687 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1688 e.preventDefault();
1689 e.stopPropagation();
1690
1691
1692 handleSearchAtlasConnect();
1693 } else {
1694 }
1695 });
1696
1697 // Also add a direct event listener as backup
1698 setTimeout(function () {
1699 var $btn = $('#connect-searchatlas-btn');
1700 if ($btn.length > 0) {
1701 $btn[0].addEventListener('click', function (e) {
1702 e.preventDefault();
1703 e.stopPropagation();
1704
1705 // Force enable the button and clean classes
1706 preventDashboardInterference($(this));
1707
1708 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1709 handleSearchAtlasConnect();
1710 }
1711 }, true); // Use capture phase to get event before other handlers
1712 }
1713 }, 500);
1714
1715 // Monitor button state changes and fix interference
1716 setTimeout(function () {
1717 var $btn = $('#connect-searchatlas-btn');
1718 if ($btn.length > 0) {
1719 // Store original button state for restoration
1720 var buttonState = {
1721 disabled: $btn.prop('disabled'),
1722 style: $btn.attr('style'),
1723 pointerEvents: $btn.css('pointer-events'),
1724 zIndex: $btn.css('z-index'),
1725 position: $btn.css('position'),
1726 classes: $btn.attr('class')
1727 };
1728
1729 // Monitor for unwanted changes to the button
1730 var observer = new MutationObserver(function (mutations) {
1731 mutations.forEach(function (mutation) {
1732 if (mutation.type === 'attributes') {
1733 // Monitor for unwanted attribute changes
1734
1735 // Fix dashboard interference automatically
1736 if (mutation.attributeName === 'class' && $btn.hasClass('dashboard-loading')) {
1737 preventDashboardInterference($btn);
1738 }
1739
1740 if (mutation.attributeName === 'disabled' && $btn.prop('disabled') && !$btn.hasClass('connecting')) {
1741 preventDashboardInterference($btn);
1742 }
1743 }
1744 });
1745 });
1746
1747 observer.observe($btn[0], {
1748 attributes: true,
1749 attributeOldValue: true,
1750 attributeFilter: ['class', 'disabled', 'style']
1751 });
1752 }
1753 }, 1000);
1754
1755 // SSO Reset button event handler
1756 $('body').on('click', '#reset-searchatlas-auth', function (e) {
1757 e.preventDefault();
1758 handleSearchAtlasResetAuth();
1759 });
1760
1761 $('body').on('click', '#local_seo_logo_close_btn', function () {
1762 removeValueById('local_seo_logo');
1763 hideElementById('local_seo_business_logo');
1764 hideElementById('local_seo_logo_close_btn');
1765 });
1766
1767 $('body').on('click', '#site_google_logo_close_btn', function () {
1768 removeValueById('site_google_logo');
1769 hideElementById('site_google_logo_img');
1770 hideElementById('site_google_logo_close_btn');
1771 });
1772
1773 $('body').on('click', '#site_social_image_close_btn', function () {
1774 removeValueById('site_social_share_image');
1775 hideElementById('site_social_share_img');
1776 hideElementById('site_social_image_close_btn');
1777 });
1778
1779 $('body').on('click', '#logo_upload_button', function () {
1780 uploadMedia('Logo', 'Add', 'local_seo_logo', 'local_seo_business_logo', 'local_seo_logo_close_btn');
1781 });
1782
1783 $('body').on('click', '#google_logo_btn', function () {
1784 uploadMedia('Site Google Logo', 'Add', 'site_google_logo', 'site_google_logo_img', 'site_google_logo_close_btn');
1785 });
1786
1787 $('body').on('click', '#social_share_image_btn', function () {
1788 uploadMedia('Site Social Share Image', 'Add', 'site_social_share_image', 'site_social_share_img', 'site_social_image_close_btn');
1789 });
1790
1791 $('body').on('click', '#robots_common1', function () {
1792 $('#robots_common1').prop('checked', true);
1793 $('#robots_common2').prop('checked', false);
1794 });
1795
1796 $('body').on('click', '#robots_common2', function () {
1797 $('#robots_common1').prop('checked', false);
1798 $('#robots_common2').prop('checked', true);
1799 });
1800
1801 addClassTableRowLocalSEO();
1802
1803 addClassTableRowSiteInfo();
1804
1805 getLocalSeoOnLoadPage();
1806
1807 siteInfoOnLoadPage();
1808
1809 $('#local_seo_person_organization').change(function () {
1810 const classes = ['17', '18', '19', '20', '21', '24', '25'];
1811 if (this.value === 'Person') {
1812 for (let i = 0; i < classes.length; i++) {
1813 $('.metasync-seo-' + classes[i]).hide();
1814 }
1815 $('.metasync-seo-15').show();
1816 } else {
1817 for (let i = 0; i < classes.length; i++) {
1818 $('.metasync-seo-' + classes[i]).show();
1819 }
1820 $('.metasync-seo-15').hide();
1821 }
1822 });
1823
1824 $('#site_info_type').change(function () {
1825 const classes = ['18', '19'];
1826 if (this.value === 'blog' || this.value === 'portfolio' || this.value === 'otherpersonal') {
1827 for (let i = 0; i < classes.length; i++) {
1828 $('.metasync-site-info-' + classes[i]).hide();
1829 }
1830 } else {
1831 for (let i = 0; i < classes.length; i++) {
1832 $('.metasync-site-info-' + classes[i]).show();
1833 }
1834 }
1835 });
1836
1837 $('#metasync-giapi-response').hide();
1838
1839 $('body').on('click', '#metasync-btn-send', function () {
1840
1841 var url = $('#metasync-giapi-url');
1842 var action = $('input[type="radio"]:checked');
1843 var response = $('#metasync-giapi-response');
1844
1845 var urls = url.val().split('\n').filter(Boolean);
1846
1847 var urls_str = urls[0];
1848 var is_bulk = false;
1849 if (urls.length > 1) {
1850 urls_str = urls;
1851 is_bulk = true;
1852 }
1853
1854 jQuery.ajax({
1855 method: 'POST',
1856 url: 'admin-ajax.php',
1857 data: {
1858 action: 'metasync_send_giapi',
1859 metasync_giapi_url: url.val(),
1860 metasync_giapi_action: action.val()
1861 }
1862 })
1863 .always(function (info) {
1864
1865 response.show();
1866
1867 $('.result-action').html('<strong>' + action.val() + '</strong>' + ' <br> ' + urls_str);
1868
1869 if (!is_bulk) {
1870 if (typeof info.error !== 'undefined') {
1871 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
1872 } else {
1873 var d = new Date();
1874 $('.result-status-code').text('Success').siblings('.result-message').text(d.toString());
1875 }
1876 } else {
1877 $('.result-status-code').text('Success').siblings('.result-message').text('Success');
1878 if (typeof info.error !== 'undefined') {
1879 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
1880 } else {
1881 $.each(info, function (index, val) {
1882
1883 if (typeof val.error !== 'undefined') {
1884 var error_code = '';
1885 if (typeof val.error.code !== 'undefined') {
1886 error_code = val.error.code;
1887 }
1888 var error_message = '';
1889 if (typeof val.error.message !== 'undefined') {
1890 error_message = val.error.message;
1891 }
1892 $('.result-status-code').text(error_code).siblings('.result-message').text(val.error.message);
1893 }
1894 });
1895 }
1896 }
1897 });
1898 });
1899
1900 $('body').on('click', '#cancel-redirection', function () {
1901 $('#add-redirection-form').hide();
1902 $('#add-redirection').focus();
1903 });
1904
1905 $('body').on('click', '.redirect_type', function () {
1906 if ($(this).val() === '410' || $(this).val() === '451') {
1907 $('#destination_url').val('');
1908 $('#destination').hide();
1909 } else {
1910 $('#destination').show();
1911 }
1912 });
1913
1914 if ($('#post_redirection').is(':checked')) {
1915 $('.hide').fadeIn('slow');
1916 }
1917 $('body').on('change', '#post_redirection', function () {
1918 if (this.checked) {
1919 $('.hide').fadeIn('slow');
1920 } else {
1921 $('.hide').fadeOut('slow');
1922 }
1923 });
1924
1925 $(document).ready(function () {
1926 if ($('#post_redirection').is(':checked')
1927 && ($('#post_redirection_type').val() === '410'
1928 || $('#post_redirection_type').val() === '451')) {
1929 $('#post_redirect_url').hide();
1930 }
1931 });
1932
1933 $('#post_redirection_type').change(function () {
1934 if ($('#post_redirection').is(':checked')
1935 && ($(this).val() === '410'
1936 || $(this).val() === '451')) {
1937 $('#post_redirect_url').hide();
1938 } else {
1939 $('#post_redirect_url').show();
1940 }
1941 });
1942
1943 });
1944
1945 $(function () {
1946 var psconsole = $('#error-code-box');
1947 if (psconsole.length) {
1948 psconsole.scrollTop(psconsole[0].scrollHeight - psconsole.height());
1949 }
1950 });
1951
1952 $(function () {
1953 $('#copy-clipboard-btn').on('click', function () {
1954 var hiddenInput = document.createElement('input');
1955 hiddenInput.setAttribute('value', document.getElementById('error-code-box').value);
1956 document.body.appendChild(hiddenInput);
1957 hiddenInput.select();
1958 document.execCommand('copy');
1959 document.body.removeChild(hiddenInput);
1960 });
1961 });
1962
1963 function dateFormat() {
1964 var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1965 var m = new Date();
1966 return months[m.getMonth()] + ' ' + ('0' + m.getDate()).slice(-2) + ', ' + m.getFullYear() + ' ' + (m.getHours() > 12 ? '0' + m.getHours() % 12 : '0' + m.getHours()).slice(-2) + ':' + ('0' + m.getMinutes()).slice(-2) + ':' + ('0' + m.getSeconds()).slice(-2) + ' ' + (m.getHours() > 12 ? 'PM' : 'AM');
1967 }
1968
1969 function sendCustomerParams(is_hb = false) {
1970 // Only show alerts for manual sync (not heartbeat)
1971 const showAlerts = !is_hb;
1972
1973 // Remove any existing sync-related notices
1974 if (showAlerts) {
1975 $('.metasync-sync-notice, .metasync-sync-error').remove();
1976 }
1977
1978 // DEBUG: Log request parameters
1979 const requestData = {
1980 action: 'metasync_send_customer_params',
1981 is_heart_beat : is_hb
1982 };
1983
1984 jQuery.ajax({
1985 type: 'post',
1986 url: 'admin-ajax.php',
1987 data: requestData,
1988 beforeSend: function () {
1989 if (showAlerts) {
1990 // Show loading state on button
1991 $('#sendAuthToken').prop('disabled', true).html('🔄 Syncing...');
1992 }
1993 },
1994 success: function (response) {
1995 // Reset button state
1996 if (showAlerts) {
1997 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
1998 }
1999
2000 if ($('#searchatlas-api-key') && $('#searchatlas-api-key').val() === '') {
2001 $('#sendAuthTokenTimestamp').html('Please save your ' + getPluginName() + ' API key');
2002 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2003
2004 // Remove stale PHP-rendered "✓ Synced" label.
2005 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2006
2007 // Update header status to "Not Synced" for missing API key
2008 updateHeaderStatus(false, 'Not Synced', 'Not Synced - API key required');
2009
2010 if (showAlerts) {
2011 showSyncError('⚠️ API Key Required', 'Please save your ' + getPluginName() + ' API key in the settings above before syncing.');
2012 }
2013
2014 } else if (response && response.throttled) {
2015 // Handle throttling response
2016 var remainingMinutes = response.remaining_minutes || 5;
2017 //$('#sendAuthTokenTimestamp').html('Please wait ' + remainingMinutes + ' minutes before syncing again');
2018 //$('#sendAuthTokenTimestamp').css({ color: 'orange' });
2019
2020 // Disable sync button and show countdown
2021 if (showAlerts) {
2022 $('#sendAuthToken').prop('disabled', true).html('⏰ Throttled (' + remainingMinutes + 'm)');
2023
2024 // Start countdown timer
2025 var countdownInterval = setInterval(function () {
2026 remainingMinutes--;
2027 if (remainingMinutes <= 0) {
2028 clearInterval(countdownInterval);
2029 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2030 $('#sendAuthTokenTimestamp').html('Ready to sync');
2031 $('#sendAuthTokenTimestamp').css({ color: 'green' });
2032 } else {
2033 $('#sendAuthToken').html('⏰ Throttled (' + remainingMinutes + 'm)');
2034 //$('#sendAuthTokenTimestamp').html('Please wait ' + remainingMinutes + ' minutes before syncing again');
2035 }
2036 }, 60000); // Update every minute
2037 }
2038
2039 // Update header status to show throttling
2040 updateHeaderStatus(false, 'Throttled', 'Throttled - Please wait ' + remainingMinutes + ' minutes');
2041
2042 if (showAlerts) {
2043 showSyncError('⏰ Request Throttled', response.message || 'Please wait ' + remainingMinutes + ' minutes before making another sync request.');
2044 }
2045
2046 } else if (response && response.detail) {
2047 $('#sendAuthTokenTimestamp').html('Please provide a valid ' + getPluginName() + ' API key');
2048 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2049
2050 // Remove stale PHP-rendered "✓ Synced" label — key is invalid on the server.
2051 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2052
2053 // Update header status to "Not Synced" for invalid API key
2054 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Invalid API key');
2055
2056 if (showAlerts) {
2057 showSyncError('❌ Invalid API Key', 'Please provide a valid ' + getPluginName() + ' API key.');
2058 }
2059
2060 } else if (response === null || !response.id) {
2061 // Update header status to "Not Synced" after failed sync
2062 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Data synchronization failed');
2063
2064 if (showAlerts) {
2065 showSyncError('❌ Sync Failed', 'Something went wrong during synchronization. Please check your connection and try again.');
2066 }
2067 // Keep existing commented behavior for timestamp
2068
2069 } else {
2070 var dateString = dateFormat();
2071 $('#sendAuthTokenTimestamp').html(dateString);
2072 $('#sendAuthTokenTimestamp').css({ color: 'green' });
2073
2074 // Update header status — check if UUID is present before declaring fully synced
2075 var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
2076 if (hasOttoUuid) {
2077 updateHeaderStatus(true, 'Synced', 'Synced - Data synchronization completed successfully');
2078 } else {
2079 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
2080 }
2081
2082 if (showAlerts) {
2083 showSyncSuccess('�
2084 Sync Complete', 'Your categories and user data have been successfully synchronized with ' + getPluginName() + '.');
2085 }
2086 }
2087 },
2088 error: function (xhr, status, error) {
2089 // Update header status to "Not Synced" for network errors
2090 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Network error during sync');
2091
2092 // Reset button state
2093 if (showAlerts) {
2094 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2095 showSyncError('❌ Network Error', 'Failed to connect to ' + getPluginName() + '. Please check your internet connection and try again.');
2096 }
2097 }
2098 });
2099 }
2100
2101 /**
2102 * Show sync success notification (wrapper for consolidated function)
2103 */
2104 function showSyncSuccess(title, message) {
2105 showPluginNotice('success', title, message, 'metasync-sync-notice', 4000);
2106 }
2107
2108 /**
2109 * Show sync error notification (wrapper for consolidated function)
2110 */
2111 function showSyncError(title, message) {
2112 showPluginNotice('error', title, message, 'metasync-sync-error', 0);
2113 }
2114
2115 function clear_otto_caches() {
2116 jQuery.ajax({
2117 url: ajaxurl,
2118 type: 'GET',
2119 data: {
2120 action: 'metasync_clear_otto_cache',
2121 clear_otto_cache: 1
2122 },
2123 success: function (response) {
2124 const now = new Date();
2125 $('#clear_otto_caches').text('Cache Cleared ' + now.toLocaleTimeString());
2126 console.log('Cleared SSR Caches');
2127 }
2128 });
2129 }
2130
2131 jQuery(document).ready(function () {
2132
2133 // sendCustomerParams();
2134 $('#sendAuthToken').on('click', function (e) {
2135 e.preventDefault();
2136 sendCustomerParams();
2137
2138 });
2139
2140 // handle otto clear cache button
2141 $('#clear_otto_caches').on('click', function (e){
2142 e.preventDefault();
2143 clear_otto_caches();
2144 });
2145
2146 // Handle General Setting Page form Submit
2147 $('#metaSyncGeneralSetting').on('submit', function (e) {
2148 // Check if this is a "Clear Error Logs" submission
2149 var formData = $(this).serialize();
2150 if (formData.indexOf('clear_log=yes') !== -1) {
2151 // This is a clear error logs submission - allow normal HTML form submission
2152 console.log('Clear Error Logs form detected - allowing HTML submission');
2153 return true; // Let the form submit normally
2154 }
2155
2156 // Check if this is a "Plugin Access Roles" submission - allow normal HTML form submission
2157 if (formData.indexOf('save_plugin_access_roles=yes') !== -1) {
2158 console.log('Plugin Access Roles form detected - allowing HTML submission');
2159 return true; // Let the form submit normally
2160 }
2161
2162 e.preventDefault(); // Prevent the default form submission for regular settings
2163 var actionField = $(this).find('input[name="action"]');
2164 var optionPage= $(this).find('input[name="option_page"]');
2165 var wpHttpReferer= $(this).find('input[name="_wp_http_referer"]');
2166 var wpnonce= $(this).find('input[name="_wpnonce"]');
2167 if(actionField.length > 0) {
2168 actionField.remove(); // Remove the action field if it exists
2169 optionPage.remove(); // Remove the action field if it exists
2170 wpHttpReferer.remove(); // Remove the action field if it exists
2171 wpnonce.remove(); // Remove the action field if it exists
2172 }
2173 // Re-serialize the form data after removing unwanted fields
2174 formData = $(this).serialize();
2175
2176 // Check if whitelabel data is in form data, if not add it manually
2177 if (formData.indexOf('whitelabel') === -1) {
2178 // Manually collect and add ALL whitelabel fields using helper function
2179 var whitelabelData = collectWhitelabelFields();
2180 if (whitelabelData) {
2181 formData += '&' + whitelabelData;
2182 }
2183 }
2184
2185 // Get current tab from URL
2186 var urlParams = new URLSearchParams(window.location.search);
2187 var currentTab = urlParams.get('tab') || 'general';
2188
2189 $.ajax({
2190 url: metaSync.ajax_url, // The AJAX URL provided by WordPress
2191 type: 'POST',
2192 data: formData + '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab), // Add the action and current tab
2193 success: function (response) {
2194 // Handle success response
2195 if(response.success){
2196 // get value of input field white_label_plugin_menu_slug
2197 const whiteLableUrl = $('#metaSyncGeneralSetting input[name="metasync_options[general][white_label_plugin_menu_slug]"]').val();
2198 // check condition if it is empty or not and redirect it
2199
2200 // add the tag query to the window location
2201 let tabParam = new URLSearchParams(window.location.search).get('tab');
2202 let tabQuery = tabParam ? '&tab=' + encodeURIComponent(tabParam) : '';
2203
2204 // Handle undefined or empty white label URL
2205 const pageSlug = (whiteLableUrl && whiteLableUrl !== '') ? whiteLableUrl : 'searchatlas';
2206 window.location = metaSync.admin_url + '?page=' + pageSlug + tabQuery;
2207 }else {
2208 // Handle error response
2209 const errors = response.data?.errors || [];
2210
2211 // Create a notice element to display the errors
2212 let html = '<div class="notice notice-error metasync-error-wrap">';
2213 if (Array.isArray(errors)) {
2214 html += '<ul>';
2215 errors.forEach(function (err) {
2216 html += '<li>' + err + '</li>';
2217 });
2218 html += '</ul>';
2219 }
2220 html += '</div>';
2221
2222 // Remove previous error notices
2223 $('.metasync-error-wrap').remove();
2224
2225 // Insert the error message before the form
2226 $('#metaSyncGeneralSetting').before(html);
2227
2228 // Scroll to the top to ensure visibility
2229 $('html, body').animate({ scrollTop: 0 }, 'slow');
2230 }
2231
2232 },
2233 error: function (error) {
2234 // Handle error response
2235 alert('There was an error saving the settings.');
2236 console.log(error);
2237 }
2238 });
2239 });
2240
2241 //hook into heartbeat-send: client will send the message 'marco' in the 'client' var inside the data array
2242 jQuery(document).on('heartbeat-send', function (e, data) {
2243 e.preventDefault();
2244
2245 // adding heart beat label
2246 sendCustomerParams(true);
2247 });
2248
2249 //hook into heartbeat-tick: client looks for a 'server' var in the data array and logs it to console
2250 jQuery(document).on('heartbeat-tick', function (e, data) {
2251 // console.log('heartbeat-tick:', data);
2252 // if(data['server'])
2253 // console.log('Server: ' + data['server']);
2254 });
2255
2256 //hook into heartbeat-error: in case of error, let's log some stuff
2257 jQuery(document).on('heartbeat-error', function (e, jqXHR, textStatus, error) {
2258 console.log('BEGIN ERROR');
2259 console.log(textStatus);
2260 console.log(error);
2261 console.log('END ERROR');
2262 });
2263
2264 // Unsaved changes warning functionality
2265 var hasUnsavedChanges = false;
2266 var initialFormData = {};
2267
2268 // Check if we're on the Advanced tab (has its own save buttons per section)
2269 function isAdvancedTab() {
2270 return window.location.href.indexOf('tab=advanced') > -1;
2271 }
2272
2273 // Initialize form change detection
2274 function initializeUnsavedChangesDetection() {
2275 // Skip unsaved changes detection on Advanced tab - it has its own section-specific save buttons
2276 if (isAdvancedTab()) {
2277 return;
2278 }
2279
2280 var $forms = $('#metaSyncGeneralSetting, #metaSyncSeoControlsForm, form[method="post"][action*="options.php"]');
2281
2282 if ($forms.length === 0) {
2283 return; // No forms to track
2284 }
2285
2286 // Store initial form data
2287 $forms.each(function () {
2288 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2289 initialFormData[formId] = $(this).serialize();
2290 });
2291
2292 // Track changes on form inputs
2293 $forms.on('input change', 'input, select, textarea', function () {
2294 checkForChanges();
2295 });
2296
2297 // Special handling for media uploads and other dynamic changes
2298 $forms.on('DOMSubtreeModified', function () {
2299 setTimeout(checkForChanges, 100); // Small delay to allow DOM changes to complete
2300 });
2301 }
2302
2303 // Check if form data has changed
2304 function checkForChanges() {
2305 // Skip on Advanced tab
2306 if (isAdvancedTab()) {
2307 return;
2308 }
2309
2310 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2311 var currentHasChanges = false;
2312
2313 $forms.each(function () {
2314 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2315 var currentData = $(this).serialize();
2316
2317 if (initialFormData[formId] && currentData !== initialFormData[formId]) {
2318 currentHasChanges = true;
2319 }
2320 });
2321
2322 hasUnsavedChanges = currentHasChanges;
2323 updateUnsavedChangesIndicator();
2324 }
2325
2326 // Update visual indicator for unsaved changes
2327 function updateUnsavedChangesIndicator() {
2328 var $saveButtons = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]');
2329 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2330
2331 if (hasUnsavedChanges) {
2332 // Add modern visual indicator to save buttons
2333 $saveButtons.each(function () {
2334 if (!$(this).find('.unsaved-indicator').length) {
2335 $(this).prepend('<span class="unsaved-indicator">●</span>');
2336 }
2337 });
2338
2339 // Add visual styling to forms
2340 $forms.addClass('has-unsaved-changes');
2341
2342 // Show sticky notification
2343 showUnsavedChangesNotification();
2344 } else {
2345 // Remove indicators
2346 $saveButtons.find('.unsaved-indicator').remove();
2347 $forms.removeClass('has-unsaved-changes');
2348
2349 // Hide sticky notification
2350 window.hideUnsavedChangesNotification();
2351 }
2352 }
2353
2354 // Show sticky notification for unsaved changes
2355 function showUnsavedChangesNotification() {
2356 var $notification = $('.metasync-unsaved-notification');
2357
2358 if ($notification.length === 0) {
2359 // Create notification if it doesn't exist
2360 var notificationHTML =
2361 '<div class="metasync-unsaved-notification">' +
2362 '<div class="notification-content">' +
2363 '<div class="notification-icon">●</div>' +
2364 '<div class="notification-message">You have unsaved changes</div>' +
2365 '</div>' +
2366 '<div class="notification-actions">' +
2367 '<button class="notification-button primary" onclick="saveChanges()">Save Now</button>' +
2368 '<button class="notification-button" onclick="discardChanges()">Discard</button>' +
2369 '<button class="close-notification" onclick="hideUnsavedChangesNotification()">×</button>' +
2370 '</div>' +
2371 '</div>';
2372
2373 $('body').append(notificationHTML);
2374 $notification = $('.metasync-unsaved-notification');
2375 }
2376
2377 // Show with animation
2378 setTimeout(function () {
2379 $notification.addClass('show');
2380 }, 100);
2381 }
2382
2383 // Hide sticky notification
2384 window.hideUnsavedChangesNotification = function () {
2385 var $notification = $('.metasync-unsaved-notification');
2386 $notification.removeClass('show');
2387 };
2388
2389 // Scroll to save button functionality
2390 window.scrollToSaveButton = function () {
2391 var $saveButton = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]').first();
2392 if ($saveButton.length) {
2393 // Hide notification temporarily while scrolling
2394 window.hideUnsavedChangesNotification();
2395
2396 $('html, body').animate({
2397 scrollTop: $saveButton.offset().top - 100
2398 }, 500, function () {
2399 // Add highlight animation
2400 $saveButton.css('animation', 'save-button-highlight 1s ease-in-out');
2401
2402 // Remove animation after it completes
2403 setTimeout(function () {
2404 $saveButton.css('animation', '');
2405 }, 1000);
2406 });
2407 }
2408 };
2409
2410 // Discard changes functionality
2411 window.discardChanges = function () {
2412 if (confirm('Are you sure you want to discard all unsaved changes? This action cannot be undone.')) {
2413 // Reload the page to discard changes
2414 window.location.reload();
2415 }
2416 };
2417
2418
2419
2420 // Warning for in-page navigation (tab links)
2421 $('.metasync-nav-tab, .nav-tab').on('click', function (e) {
2422 if (hasUnsavedChanges) {
2423 var tabName = $(this).text().trim();
2424 var confirmed = confirm('⚠️ Unsaved Changes Alert\n\nYou have unsaved changes that will be lost if you navigate to "' + tabName + '".\n\nWould you like to:\n• Click "Cancel" to stay and save your changes\n• Click "OK" to discard changes and continue');
2425 if (!confirmed) {
2426 e.preventDefault();
2427 return false;
2428 } else {
2429 // If user confirms, clear the unsaved changes state
2430 hasUnsavedChanges = false;
2431 updateUnsavedChangesIndicator();
2432 }
2433 }
2434 });
2435
2436 // Clear unsaved changes flag when form is successfully submitted
2437 $('#metaSyncGeneralSetting').on('submit', function () {
2438 // Form submission handler already exists above, so we just need to listen for successful response
2439 var originalAjaxHandler = $(this).data('events') && $(this).data('events').submit;
2440 });
2441
2442 // Listen for successful form submission to clear the unsaved changes flag
2443 $(document).ajaxSuccess(function (event, xhr, settings) {
2444 if (settings.data && typeof settings.data === 'string' && settings.data.indexOf('action=meta_sync_save_settings') > -1) {
2445 try {
2446 var response = JSON.parse(xhr.responseText);
2447 if (response.success) {
2448 hasUnsavedChanges = false;
2449 updateUnsavedChangesIndicator();
2450 // Update initial form data after successful save
2451 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2452 $forms.each(function () {
2453 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2454 initialFormData[formId] = $(this).serialize();
2455 });
2456 }
2457 } catch (e) {
2458 // Response is not JSON, ignore
2459 }
2460 }
2461 });
2462
2463 // Save Changes function - use AJAX instead of form submission
2464 window.saveChanges = function () {
2465 isSaving = true;
2466
2467 // Completely remove the floating notification immediately when clicked
2468 var $notification = $('.metasync-unsaved-notification');
2469 if ($notification.length > 0) {
2470 $notification.remove(); // Completely remove from DOM, no animations
2471 }
2472
2473 var $form = $('#metaSyncGeneralSetting');
2474 if ($form.length > 0) {
2475 // Get form data and submit via AJAX
2476 var formData = $form.serialize();
2477
2478 // Ensure ALL whitelabel data is included using helper function
2479 if (formData.indexOf('whitelabel') === -1) {
2480 var whitelabelData = collectWhitelabelFields();
2481 if (whitelabelData) {
2482 formData += '&' + whitelabelData;
2483 }
2484 }
2485
2486 // Get current tab from URL
2487 var urlParams = new URLSearchParams(window.location.search);
2488 var currentTab = urlParams.get('tab') || 'general';
2489
2490 formData += '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab);
2491
2492 $.ajax({
2493 url: metaSync.ajax_url,
2494 type: 'POST',
2495 data: formData,
2496 success: function (response) {
2497 if (response.success) {
2498 // Clear unsaved changes flag
2499 hasUnsavedChanges = false;
2500 updateUnsavedChangesIndicator();
2501
2502 // Show temporary success indication in plugin area
2503 var successNotice = '<div class="notice notice-success is-dismissible metasync-save-notice" style="margin: 20px 0; padding: 12px;"><p><strong>�
2504 Settings saved successfully!</strong></p></div>';
2505
2506 // Insert between navigation menu and page content
2507 var $navWrapper = $('.metasync-nav-wrapper');
2508 if ($navWrapper.length > 0) {
2509 // Position after navigation menu but before first dashboard card or form
2510 $navWrapper.after(successNotice);
2511 } else {
2512 // Fallback: insert at top of settings page
2513 $('.metasync-dashboard-wrap').prepend(successNotice);
2514 }
2515
2516 // Scroll to the success message for better visibility
2517 $('html, body').animate({ scrollTop: 0 }, 'slow');
2518
2519 setTimeout(function () {
2520 $('.metasync-save-notice').fadeOut(300, function () {
2521 $(this).remove();
2522 });
2523 }, 3000);
2524 } else {
2525 // Handle validation errors
2526 var errors = response.data && response.data.errors ? response.data.errors : [];
2527 var errorHtml = '<div class="notice notice-error metasync-error-wrap" style="margin: 20px; padding: 12px;">';
2528 if (Array.isArray(errors)) {
2529 errorHtml += '<ul>';
2530 for (var i = 0; i < errors.length; i++) {
2531 errorHtml += '<li>' + errors[i] + '</li>';
2532 }
2533 errorHtml += '</ul>';
2534 } else {
2535 var message = 'An error occurred while saving settings.';
2536 if (response.data && response.data.message) {
2537 message = response.data.message;
2538 }
2539 errorHtml += '<p>' + message + '</p>';
2540 }
2541 errorHtml += '</div>';
2542
2543 // Insert error notice in plugin area
2544 var $navWrapper = $('.metasync-nav-wrapper');
2545 if ($navWrapper.length > 0) {
2546 $navWrapper.after(errorHtml);
2547 } else {
2548 // Fallback: insert at top of plugin content area
2549 $('.metasync-dashboard-wrap').prepend(errorHtml);
2550 }
2551
2552 // Scroll to the error message for better visibility
2553 $('html, body').animate({ scrollTop: 0 }, 'slow');
2554
2555 setTimeout(function () {
2556 $('.metasync-error-wrap').fadeOut(300, function () {
2557 $(this).remove();
2558 });
2559 }, 5000);
2560 }
2561 isSaving = false;
2562 },
2563 error: function (xhr, status, error) {
2564 // Handle AJAX error
2565 var errorMessage = 'There was an error saving the settings. Please try again.';
2566 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
2567 errorMessage = xhr.responseJSON.data.message;
2568 }
2569
2570 var ajaxErrorNotice = '<div class="notice notice-error is-dismissible metasync-ajax-error" style="margin: 20px 0; padding: 12px;"><p><strong>❌ Error:</strong> ' + errorMessage + '</p></div>';
2571
2572 // Insert between navigation menu and page content
2573 var $navWrapper = $('.metasync-nav-wrapper');
2574 if ($navWrapper.length > 0) {
2575 // Position after navigation menu but before first dashboard card or form
2576 $navWrapper.after(ajaxErrorNotice);
2577 } else {
2578 // Fallback: insert at top of settings page
2579 $('.metasync-dashboard-wrap').prepend(ajaxErrorNotice);
2580 }
2581
2582 // Scroll to the error message for better visibility
2583 $('html, body').animate({ scrollTop: 0 }, 'slow');
2584
2585 setTimeout(function () {
2586 $('.metasync-ajax-error').fadeOut(300, function () {
2587 $(this).remove();
2588 });
2589 }, 5000);
2590
2591 isSaving = false;
2592 }
2593 });
2594 }
2595 };
2596
2597 // Enhanced beforeunload message (disabled when saving)
2598 var isSaving = false;
2599 $(window).on('beforeunload', function (e) {
2600 if (hasUnsavedChanges && !isSaving) {
2601 var message = '🔄 You have unsaved changes in MetaSync settings that will be lost if you leave this page.';
2602 e.returnValue = message; // For older browsers
2603 return message;
2604 }
2605 });
2606
2607 // Initialize the detection when page loads
2608 setTimeout(initializeUnsavedChangesDetection, 1000); // Small delay to ensure all elements are loaded
2609
2610 // Indexation Control form AJAX save functionality
2611 function initializeSeoControlsSaveHandler() {
2612 // Only initialize if we're on the Indexation Control page
2613 if ($('#metaSyncSeoControlsForm').length > 0) {
2614 // Override the saveChanges function for Indexation Control form
2615 window.saveChanges = function () {
2616 var $form = $('#metaSyncSeoControlsForm');
2617 if ($form.length > 0) {
2618 // Get form data and submit via AJAX
2619 var formData = $form.serialize();
2620 formData += '&action=meta_sync_save_seo_controls';
2621
2622 // Remove the notification immediately
2623 var $notification = $('.metasync-unsaved-notification');
2624 if ($notification.length > 0) {
2625 $notification.remove();
2626 }
2627
2628 // Clear previous messages
2629 $('#seo-controls-messages').empty();
2630
2631 $.ajax({
2632 url: ajaxurl || metaSync.ajax_url,
2633 type: 'POST',
2634 data: formData,
2635 dataType: 'json',
2636 success: function (response) {
2637 if (response.success) {
2638 // Show success message above Indexation Control section
2639 $('#seo-controls-messages').html(
2640 '<div class="notice notice-success is-dismissible" style="margin-bottom: 20px;">' +
2641 '<p><strong>�
2642 Success!</strong> ' + response.data.message + '</p>' +
2643 '</div>'
2644 );
2645
2646 // Clear unsaved changes state
2647 hasUnsavedChanges = false;
2648 if (typeof updateUnsavedChangesIndicator === 'function') {
2649 updateUnsavedChangesIndicator();
2650 }
2651
2652 // Update initial form data to current state after successful save
2653 var formId = $form.attr('id') || 'metaSyncSeoControlsForm';
2654 initialFormData[formId] = $form.serialize();
2655
2656 // Auto-hide success notice after 5 seconds
2657 setTimeout(function () {
2658 $('#seo-controls-messages .notice-success').fadeOut();
2659 }, 5000);
2660 } else {
2661 // Show error message above Indexation Control section
2662 $('#seo-controls-messages').html(
2663 '<div class="notice notice-error is-dismissible" style="margin-bottom: 20px;">' +
2664 '<p><strong>❌ Error!</strong> ' + (response.data.message || 'Failed to save settings') + '</p>' +
2665 '</div>'
2666 );
2667 }
2668
2669 // Make dismiss buttons work
2670 $('#seo-controls-messages .notice-dismissible').each(function () {
2671 var $notice = $(this);
2672 if (!$notice.find('.notice-dismiss').length) {
2673 $notice.append('<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>');
2674 }
2675 $notice.find('.notice-dismiss').on('click', function () {
2676 $notice.fadeOut();
2677 });
2678 });
2679 },
2680 error: function (xhr, status, error) {
2681 // Show error message above Indexation Control section
2682 $('#seo-controls-messages').html(
2683 '<div class="notice notice-error is-dismissible" style="margin-bottom: 20px;">' +
2684 '<p><strong>❌ Error!</strong> Network error occurred while saving. Please check your connection and try again.</p>' +
2685 '</div>'
2686 );
2687 console.error('AJAX Error:', error);
2688 console.error('XHR Status:', xhr.status);
2689 console.error('XHR Response:', xhr.responseText);
2690
2691 // Don't clear unsaved changes on network error
2692 // User may want to retry or fix the issue
2693 },
2694 complete: function() {
2695 // Re-enable save button if it was disabled
2696 $('.metasync-save-button').prop('disabled', false);
2697 }
2698 });
2699 } else {
2700 // Fallback to regular form submission
2701 $('form[method="post"]').first().submit();
2702 }
2703 };
2704 }
2705 }
2706
2707 // Initialize Indexation Control functionality
2708 initializeSeoControlsSaveHandler();
2709
2710 /**
2711 * Handle "All Roles" checkbox behavior for Content Genius sync
2712 * When "All Roles" is checked, uncheck all other role checkboxes
2713 * When any specific role is checked, uncheck "All Roles"
2714 *
2715 * @since 1.0.0
2716 * @returns {void}
2717 */
2718 function initializeContentGeniusRoleCheckboxes() {
2719 try {
2720 // Cache selectors for better performance
2721 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
2722 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
2723
2724 // Exit early if elements don't exist
2725 if (allRolesCheckbox.length === 0 && roleCheckboxes.length === 0) {
2726 return;
2727 }
2728
2729 // When "All Roles" is checked, uncheck all other checkboxes
2730 if (allRolesCheckbox.length > 0) {
2731 allRolesCheckbox.off('change.metasyncRoles').on('change.metasyncRoles', function () {
2732 try {
2733 if ($(this).is(':checked')) {
2734 roleCheckboxes.prop('checked', false);
2735 // Visual feedback
2736 roleCheckboxes.closest('.metasync-role-option').removeClass('active');
2737 $(this).closest('.metasync-role-option-all').addClass('active');
2738 } else {
2739 $(this).closest('.metasync-role-option-all').removeClass('active');
2740 }
2741 } catch (e) {
2742 console.warn('MetaSync: Error handling "All Roles" checkbox change:', e);
2743 }
2744 });
2745 }
2746
2747 // When any specific role is checked, uncheck "All Roles"
2748 if (roleCheckboxes.length > 0) {
2749 roleCheckboxes.off('change.metasyncRoles').on('change.metasyncRoles', function () {
2750 try {
2751 if ($(this).is(':checked')) {
2752 allRolesCheckbox.prop('checked', false);
2753 allRolesCheckbox.closest('.metasync-role-option-all').removeClass('active');
2754 // Visual feedback
2755 $(this).closest('.metasync-role-option').addClass('active');
2756 } else {
2757 $(this).closest('.metasync-role-option').removeClass('active');
2758 }
2759 } catch (e) {
2760 console.warn('MetaSync: Error handling role checkbox change:', e);
2761 }
2762 });
2763 }
2764
2765 // Initialize active states on page load
2766 initializeRoleCheckboxStates();
2767
2768 } catch (error) {
2769 console.error('MetaSync: Failed to initialize Content Genius role checkboxes:', error);
2770 }
2771 }
2772
2773 /**
2774 * Initialize active states for role checkboxes on page load
2775 *
2776 * @since 1.0.0
2777 * @returns {void}
2778 */
2779 function initializeRoleCheckboxStates() {
2780 try {
2781 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
2782 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
2783
2784 // Set active state for "All Roles" if checked
2785 if (allRolesCheckbox.is(':checked')) {
2786 allRolesCheckbox.closest('.metasync-role-option-all').addClass('active');
2787 }
2788
2789 // Set active state for checked roles
2790 roleCheckboxes.each(function () {
2791 if ($(this).is(':checked')) {
2792 $(this).closest('.metasync-role-option').addClass('active');
2793 }
2794 });
2795 } catch (e) {
2796 console.warn('MetaSync: Error initializing role checkbox states:', e);
2797 }
2798 }
2799
2800 // Initialize Content Genius role checkboxes with safety wrapper
2801 if (typeof $ !== 'undefined' && $.fn) {
2802 initializeContentGeniusRoleCheckboxes();
2803 } else {
2804 console.warn('MetaSync: jQuery not available for role checkbox initialization');
2805 }
2806
2807 // ========================================
2808 // SETTINGS ACCORDION FUNCTIONALITY
2809 // ========================================
2810
2811 /**
2812 * Initialize accordion sections with localStorage state persistence
2813 */
2814 function initSettingsAccordion() {
2815 var $accordionSections = $('.metasync-accordion-section');
2816
2817 if ($accordionSections.length === 0) {
2818 return; // No accordion on this page
2819 }
2820
2821 console.log('🎨 Initializing settings accordion with ' + $accordionSections.length + ' sections');
2822
2823 // Restore saved state from localStorage
2824 restoreAccordionState();
2825
2826 // Click handler for accordion headers
2827 $('.metasync-accordion-header').on('click', function (e) {
2828 toggleAccordionSection($(this));
2829 });
2830
2831 // Keyboard navigation (Enter/Space)
2832 $('.metasync-accordion-header').on('keydown', function (e) {
2833 if (e.key === 'Enter' || e.key === ' ') {
2834 e.preventDefault();
2835 toggleAccordionSection($(this));
2836 }
2837 });
2838 }
2839
2840 /**
2841 * Toggle accordion section open/closed
2842 * @param {jQuery} $header - The clicked header element
2843 */
2844 function toggleAccordionSection($header) {
2845 var $section = $header.closest('.metasync-accordion-section');
2846 var $content = $section.find('.metasync-accordion-content');
2847 var isOpen = $header.attr('aria-expanded') === 'true';
2848 var sectionKey = $section.data('section');
2849
2850 if (isOpen) {
2851 // Close section
2852 $header.attr('aria-expanded', 'false');
2853 $content.attr('data-state', 'closed');
2854 console.log('📁 Closed accordion section: ' + sectionKey);
2855 } else {
2856 // Open section
2857 $header.attr('aria-expanded', 'true');
2858 $content.attr('data-state', 'open');
2859 console.log('📂 Opened accordion section: ' + sectionKey);
2860 }
2861
2862 // Save state to localStorage
2863 saveAccordionState();
2864 }
2865
2866 /**
2867 * Save accordion state to localStorage
2868 */
2869 function saveAccordionState() {
2870 var state = {};
2871
2872 $('.metasync-accordion-section').each(function () {
2873 var sectionKey = $(this).data('section');
2874 var isOpen = $(this).find('.metasync-accordion-header').attr('aria-expanded') === 'true';
2875 state[sectionKey] = isOpen;
2876 });
2877
2878 try {
2879 localStorage.setItem('metasync_accordion_state', JSON.stringify(state));
2880 } catch (e) {
2881 console.warn('⚠️ Could not save accordion state to localStorage:', e);
2882 }
2883 }
2884
2885 /**
2886 * Restore accordion state from localStorage
2887 */
2888 function restoreAccordionState() {
2889 try {
2890 var savedState = localStorage.getItem('metasync_accordion_state');
2891 if (!savedState) {
2892 return; // No saved state, use defaults
2893 }
2894
2895 var state = JSON.parse(savedState);
2896
2897 $('.metasync-accordion-section').each(function () {
2898 var $section = $(this);
2899 var sectionKey = $section.data('section');
2900 var $header = $section.find('.metasync-accordion-header');
2901 var $content = $section.find('.metasync-accordion-content');
2902
2903 if (Object.prototype.hasOwnProperty.call(state, sectionKey)) {
2904 var shouldBeOpen = state[sectionKey];
2905 $header.attr('aria-expanded', shouldBeOpen ? 'true' : 'false');
2906 $content.attr('data-state', shouldBeOpen ? 'open' : 'closed');
2907 }
2908 });
2909
2910 console.log('💾 Restored accordion state from localStorage');
2911 } catch (e) {
2912 console.warn('⚠️ Could not restore accordion state:', e);
2913 }
2914 }
2915
2916 // Initialize accordion when DOM is ready
2917 initSettingsAccordion();
2918
2919 // ========================================
2920 // TOOLTIP SYSTEM
2921 // ========================================
2922
2923 /**
2924 * Initialize tooltip functionality
2925 */
2926 function initTooltipSystem() {
2927 console.log('🔍 Tooltip system initialization started');
2928
2929 var $tooltipTriggers = $('.metasync-tooltip-trigger');
2930 var currentTooltip = null;
2931
2932 console.log('🔍 Found ' + $tooltipTriggers.length + ' tooltip triggers');
2933
2934 if ($tooltipTriggers.length === 0) {
2935 console.warn('⚠️ No tooltip triggers found on this page');
2936 return; // No tooltips on this page
2937 }
2938
2939 console.log('💡 Initializing tooltip system with ' + $tooltipTriggers.length + ' tooltips');
2940
2941 // Click handler for tooltip triggers
2942 $tooltipTriggers.on('click', function (e) {
2943 e.preventDefault();
2944 e.stopPropagation();
2945
2946 var $trigger = $(this);
2947 var tooltipId = $trigger.data('tooltip-id');
2948 var $tooltip = $('#tooltip-' + tooltipId);
2949
2950 console.log('🖱️ Tooltip trigger clicked:', tooltipId);
2951 console.log('🎯 Tooltip element found:', $tooltip.length);
2952
2953 // Close other tooltips
2954 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
2955 currentTooltip.removeClass('show');
2956 }
2957
2958 // Toggle current tooltip
2959 if ($tooltip.hasClass('show')) {
2960 $tooltip.removeClass('show');
2961 currentTooltip = null;
2962 } else {
2963 $tooltip.addClass('show');
2964 currentTooltip = $tooltip;
2965 positionTooltip($trigger, $tooltip);
2966 }
2967 });
2968
2969 // Hover handler (desktop only)
2970 var hideTimeout;
2971
2972 if (window.innerWidth > 768) {
2973 // Show tooltip on trigger hover
2974 $tooltipTriggers.on('mouseenter', function () {
2975 var $trigger = $(this);
2976 var tooltipId = $trigger.data('tooltip-id');
2977 var $tooltip = $('#tooltip-' + tooltipId);
2978
2979 // Clear any pending hide timeout
2980 clearTimeout(hideTimeout);
2981
2982 console.log('🖱️ Hover on trigger:', tooltipId);
2983
2984 // Close other tooltips
2985 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
2986 currentTooltip.removeClass('show');
2987 }
2988
2989 $tooltip.addClass('show');
2990 currentTooltip = $tooltip;
2991 positionTooltip($trigger, $tooltip);
2992 });
2993
2994 // Start hide timer when leaving trigger
2995 $tooltipTriggers.on('mouseleave', function () {
2996 var tooltipId = $(this).data('tooltip-id');
2997 var $tooltip = $('#tooltip-' + tooltipId);
2998
2999 console.log('🖱️ Mouse left trigger:', tooltipId);
3000
3001 // Delay hiding to allow moving to tooltip
3002 hideTimeout = setTimeout(function () {
3003 // Only hide if not hovering tooltip
3004 if (!$tooltip.is(':hover')) {
3005 console.log('⏱️ Hiding tooltip:', tooltipId);
3006 $tooltip.removeClass('show');
3007 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3008 currentTooltip = null;
3009 }
3010 } else {
3011 console.log('✋ Mouse is over tooltip, keeping visible');
3012 }
3013 }, 200);
3014 });
3015
3016 // Cancel hide when entering tooltip
3017 $('.metasync-tooltip').on('mouseenter', function () {
3018 console.log('🎯 Mouse entered tooltip');
3019 clearTimeout(hideTimeout);
3020 $(this).addClass('show');
3021 });
3022
3023 // Hide when leaving tooltip
3024 $('.metasync-tooltip').on('mouseleave', function () {
3025 console.log('🎯 Mouse left tooltip');
3026 var $tooltip = $(this);
3027
3028 hideTimeout = setTimeout(function () {
3029 var tooltipId = $tooltip.attr('id').replace('tooltip-', '');
3030 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3031
3032 // Only hide if not hovering trigger
3033 if (!$trigger.is(':hover')) {
3034 console.log('⏱️ Hiding tooltip from tooltip leave');
3035 $tooltip.removeClass('show');
3036 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3037 currentTooltip = null;
3038 }
3039 } else {
3040 console.log('✋ Mouse is back on trigger, keeping visible');
3041 }
3042 }, 200);
3043 });
3044 }
3045
3046 // Close tooltip when clicking outside
3047 $(document).on('click', function (e) {
3048 if (!$(e.target).closest('.metasync-tooltip-trigger, .metasync-tooltip').length) {
3049 if (currentTooltip) {
3050 currentTooltip.removeClass('show');
3051 currentTooltip = null;
3052 }
3053 }
3054 });
3055
3056 // Keyboard accessibility - ESC to close
3057 $(document).on('keydown', function (e) {
3058 if (e.key === 'Escape' && currentTooltip) {
3059 currentTooltip.removeClass('show');
3060 currentTooltip = null;
3061 }
3062 });
3063
3064 // Reposition tooltips on window resize
3065 $(window).on('resize', function () {
3066 if (currentTooltip) {
3067 var tooltipId = currentTooltip.attr('id').replace('tooltip-', '');
3068 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3069 positionTooltip($trigger, currentTooltip);
3070 }
3071 });
3072 }
3073
3074 /**
3075 * Position tooltip relative to trigger
3076 * @param {jQuery} $trigger - The trigger button
3077 * @param {jQuery} $tooltip - The tooltip element
3078 */
3079 function positionTooltip($trigger, $tooltip) {
3080 // Skip positioning on mobile (uses fixed positioning)
3081 if (window.innerWidth <= 768) {
3082 return;
3083 }
3084
3085 var triggerRect = $trigger[0].getBoundingClientRect();
3086 var tooltipWidth = $tooltip.outerWidth();
3087 var viewportWidth = $(window).width();
3088 var spaceRight = viewportWidth - triggerRect.right;
3089
3090 // Check if tooltip would overflow on the right
3091 if (spaceRight < tooltipWidth + 20) {
3092 // Position on the left side
3093 $tooltip.attr('data-position', 'left');
3094 } else {
3095 // Position on the right side (default)
3096 $tooltip.attr('data-position', 'right');
3097 }
3098 }
3099
3100 // Initialize tooltip system
3101 initTooltipSystem();
3102
3103 // PR3: Burst ping — 10 min polling when UNREGISTERED or KEY_PENDING. Stop after 5 failed attempts.
3104 (function () {
3105 if (typeof metaSync === 'undefined' || !metaSync.heartbeat_state) return;
3106 var state = metaSync.heartbeat_state;
3107 if (state !== 'UNREGISTERED' && state !== 'KEY_PENDING') return;
3108
3109 var INTERVAL_MS = 10 * 60 * 1000;
3110 var MAX_ATTEMPTS = 5;
3111 var attempts = 0;
3112 var lastState = state;
3113 var intervalId = null;
3114
3115 function stop() {
3116 if (intervalId) {
3117 clearInterval(intervalId);
3118 intervalId = null;
3119 }
3120 }
3121
3122 intervalId = setInterval(function () {
3123 $.post(metaSync.ajax_url, {
3124 action: 'metasync_burst_ping',
3125 nonce: metaSync.burst_ping_nonce
3126 })
3127 .done(function (res) {
3128 if (!res || !res.data) {
3129 attempts++;
3130 if (attempts >= MAX_ATTEMPTS) stop();
3131 return;
3132 }
3133 var data = res.data;
3134 var newState = data.state || lastState;
3135 if (newState !== lastState) {
3136 attempts = 0;
3137 lastState = newState;
3138 }
3139 if (data.heartbeat_confirmed || newState === 'CONNECTED') {
3140 stop();
3141 if (newState === 'CONNECTED' && typeof updateHeaderStatus === 'function') {
3142 var hasOttoUuid = typeof metaSync !== 'undefined' && metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
3143 if (hasOttoUuid) {
3144 updateHeaderStatus(true, 'Synced', 'Heartbeat confirmed');
3145 } else {
3146 updateHeaderStatus(false, 'Warning', 'Connected but OTTO UUID is missing — deploys will not work. Please reconnect.');
3147 }
3148 }
3149 return;
3150 }
3151 attempts++;
3152 if (attempts >= MAX_ATTEMPTS) stop();
3153 })
3154 .fail(function () {
3155 attempts++;
3156 if (attempts >= MAX_ATTEMPTS) stop();
3157 });
3158 }, INTERVAL_MS);
3159 })();
3160
3161 });
3162
3163 })(jQuery);
3164