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

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