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

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

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