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

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