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

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

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