| @@ -49,8 +49,44 @@ | ||
| 49 | 49 | return window.MetasyncConfig && window.MetasyncConfig.ottoName ? window.MetasyncConfig.ottoName : 'OTTO'; |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | 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 | + /** | |
| 53 | 89 | * Update integration status indicator in header |
| 54 | 90 | * @param {boolean} isIntegrated - Whether the integration is active |
| 55 | 91 | * @param {string} statusText - Status text to display |
| 56 | 92 | * @param {string} titleText - Tooltip text |
| @@ -235,9 +271,10 @@ | ||
| 235 | 271 | |
| 236 | 272 | function metasyncLGLogin(user, pass) { |
| 237 | 273 | jQuery.post(ajaxurl, { |
| 238 | 274 | action: 'metasync_lglogin', |
| 239 | - username: user, password: pass | |
| 275 | + username: user, password: pass, | |
| 276 | + nonce: metaSync.lglogin_nonce || '' | |
| 240 | 277 | }, function (response) { |
| 241 | 278 | if (typeof response.token !== 'undefined') { |
| 242 | 279 | $('#linkgraph_token').val(response.token); |
| 243 | 280 | $('#linkgraph_customer_id').val(response.customer_id); |
| @@ -470,9 +507,9 @@ | ||
| 470 | 507 | |
| 471 | 508 | function resetConnectButton() { |
| 472 | 509 | var $button = $('#connect-searchatlas-btn'); |
| 473 | 510 | var $progressContainer = $('.metasync-sa-connect-progress'); |
| 474 | - var hasApiKey = $('#searchatlas-api-key').val().trim() !== ''; | |
| 511 | + var hasApiKey = searchAtlasKeyConfigured(); | |
| 475 | 512 | |
| 476 | 513 | $button.prop('disabled', false) |
| 477 | 514 | .removeClass('connecting authenticating success dashboard-loading') // Remove all loading classes |
| 478 | 515 | .html(hasApiKey ? '🔄 Re-authenticate with ' + getPluginName() : '🔗 Connect to ' + getPluginName()); |
| @@ -480,9 +517,14 @@ | ||
| 480 | 517 | } |
| 481 | 518 | |
| 482 | 519 | function startSearchAtlasPolling(nonceToken) { |
| 483 | 520 | var pollCount = 0; |
| 484 | - var maxPolls = 12; // Poll for 60 seconds (12 * 5 seconds) | |
| 521 | + // WP-558 (Option A): poll for 2 minutes (24 * 5s). Each poll also drives | |
| 522 | + // the outbound pull-based connect fallback server-side, which needs a | |
| 523 | + // slightly longer window than the legacy push-only 60s to ride out the | |
| 524 | + // human SSO-authorization delay. ~24 requests stays inside the CA | |
| 525 | + // endpoint's 120/hour per-domain budget. | |
| 526 | + var maxPolls = 24; // Poll for 120 seconds (24 * 5 seconds) | |
| 485 | 527 | var $progressContainer = $('.metasync-sa-connect-progress'); |
| 486 | 528 | var $progressFill = $('.metasync-sa-connect-progress-fill'); |
| 487 | 529 | var $progressText = $('.metasync-sa-connect-progress-text'); |
| 488 | 530 | var $button = $('#connect-searchatlas-btn'); |
| @@ -521,20 +563,20 @@ | ||
| 521 | 563 | |
| 522 | 564 | // ✅ Reset the authentication flow while keeping the timeout component |
| 523 | 565 | resetConnectButton(); |
| 524 | 566 | |
| 525 | - showConnectError('⏰ Authentication Timeout', | |
| 526 | - 'The authentication process timed out after 60 seconds. Please complete the authentication more quickly or check for network issues.', | |
| 567 | + showConnectError('⏰ Authentication Timeout', | |
| 568 | + 'The authentication process timed out after 2 minutes. Please complete the authentication more quickly or check for network issues.', | |
| 527 | 569 | [{ |
| 528 | 570 | text: '🔄 Try Again', |
| 529 | 571 | action: function () { |
| 530 | - handleSearchAtlasConnect(); | |
| 572 | + handleSearchAtlasConnect(); | |
| 531 | 573 | } |
| 532 | 574 | }, { |
| 533 | 575 | text: '💬 Contact Support', |
| 534 | - action: function () { | |
| 576 | + action: function () { | |
| 535 | 577 | var supportEmail = metaSync.support_email || '[email protected]'; |
| 536 | - window.open('mailto:' + supportEmail + '?subject=Connect Authentication Timeout (30s)', '_blank'); | |
| 578 | + window.open('mailto:' + supportEmail + '?subject=Connect Authentication Timeout (120s)', '_blank'); | |
| 537 | 579 | } |
| 538 | 580 | }] |
| 539 | 581 | ); |
| 540 | 582 | return; |
| @@ -569,9 +611,9 @@ | ||
| 569 | 611 | |
| 570 | 612 | // Handle different status codes with enhanced UX |
| 571 | 613 | if (statusCode === 200) { |
| 572 | 614 | // Success: Update all UI elements to reflect connected state |
| 573 | - updateUIForConnectedState(response.data.api_key, response.data.otto_pixel_uuid); | |
| 615 | + updateUIForConnectedState(response.data.masked_key, response.data.otto_pixel_uuid); | |
| 574 | 616 | |
| 575 | 617 | // Refresh Plugin Auth Token field to show the auto-generated token |
| 576 | 618 | // This will either show the existing token or the newly auto-generated one |
| 577 | 619 | $.ajax({ |
| @@ -602,9 +644,9 @@ | ||
| 602 | 644 | $button.closest('.metasync-sa-connect-container').removeClass('metasync-sa-connect-success-animation'); |
| 603 | 645 | }, 600); |
| 604 | 646 | |
| 605 | 647 | // Track 1-click activation in GA4 |
| 606 | - var hasExistingApiKey = $('#searchatlas-api-key').val() && $('#searchatlas-api-key').val().trim() !== ''; | |
| 648 | + var hasExistingApiKey = searchAtlasKeyConfigured(); | |
| 607 | 649 | $.ajax({ |
| 608 | 650 | url: metaSync.ajax_url, |
| 609 | 651 | type: 'POST', |
| 610 | 652 | data: { |
| @@ -737,9 +779,9 @@ | ||
| 737 | 779 | var timeElapsed = currentPoll * 5; |
| 738 | 780 | var timeRemaining = (maxPolls - currentPoll) * 5; |
| 739 | 781 | $progressTime.text(timeElapsed + 's elapsed, ' + timeRemaining + 's remaining'); |
| 740 | 782 | |
| 741 | - // Update progress text based on time elapsed (optimized for 60-second timeout) | |
| 783 | + // Update progress text based on time elapsed (optimized for 120-second timeout) | |
| 742 | 784 | var progressMessages = [ |
| 743 | 785 | 'Establishing connection and opening authentication window...', |
| 744 | 786 | 'Please complete authentication in the popup window...', |
| 745 | 787 | 'Almost done! Finalizing your authentication...' |
| @@ -879,9 +921,18 @@ | ||
| 879 | 921 | }, { |
| 880 | 922 | text: '📚 Learn More About Registration', |
| 881 | 923 | action: function () { |
| 882 | 924 | var docDomain = metaSync.documentation_domain || 'https://searchatlas.com'; |
| 883 | - window.open(docDomain, '_blank'); | |
| 925 | + try { | |
| 926 | + var parsedDoc = new URL(docDomain); | |
| 927 | + if (parsedDoc.protocol === 'https:' || parsedDoc.protocol === 'http:') { | |
| 928 | + var a = document.createElement('a'); | |
| 929 | + a.href = parsedDoc.href; | |
| 930 | + a.target = '_blank'; | |
| 931 | + a.rel = 'noopener'; | |
| 932 | + a.click(); | |
| 933 | + } | |
| 934 | + } catch (e) { /* invalid URL */ } | |
| 884 | 935 | } |
| 885 | 936 | }, { |
| 886 | 937 | text: '🔄 Try Authentication Again', |
| 887 | 938 | action: function () { |
| @@ -963,9 +1014,9 @@ | ||
| 963 | 1014 | 'Check if firewall is blocking the request' |
| 964 | 1015 | ]; |
| 965 | 1016 | } else if (context === 'timeout') { |
| 966 | 1017 | recoverySuggestions = [ |
| 967 | - 'Complete authentication within 60 seconds', | |
| 1018 | + 'Complete authentication within 2 minutes', | |
| 968 | 1019 | 'Check if the popup window needs attention', |
| 969 | 1020 | 'Ensure you have your ' + getPluginName() + ' login ready', |
| 970 | 1021 | 'Try the authentication process again', |
| 971 | 1022 | 'Contact support if timeouts persist' |
| @@ -1095,17 +1146,17 @@ | ||
| 1095 | 1146 | |
| 1096 | 1147 | // Add connection status indicator |
| 1097 | 1148 | function updateConnectionStatus() { |
| 1098 | 1149 | var $button = $('#connect-searchatlas-btn'); |
| 1099 | - var $apiKeyField = $('#searchatlas-api-key'); | |
| 1100 | - | |
| 1101 | - // Only update status if we're on a page with the API key field (General Settings) | |
| 1150 | + var $apiKeyField = $('#searchatlas-api-key-display'); | |
| 1151 | + | |
| 1152 | + // Only update status if we're on a page with the API key display (General Settings) | |
| 1102 | 1153 | // On other pages, preserve the PHP-determined status in the header |
| 1103 | 1154 | if ($apiKeyField.length === 0) { |
| 1104 | - return; // Don't update status on pages without the API key field | |
| 1155 | + return; // Don't update status on pages without the API key display | |
| 1105 | 1156 | } |
| 1106 | - | |
| 1107 | - var hasApiKey = $apiKeyField.val() && $apiKeyField.val().trim() !== ''; | |
| 1157 | + | |
| 1158 | + var hasApiKey = searchAtlasKeyConfigured(); | |
| 1108 | 1159 | var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== ''; |
| 1109 | 1160 | var isFullyConnected = hasApiKey && hasOttoUuid; |
| 1110 | 1161 | |
| 1111 | 1162 | // Update button text based on connection state |
| @@ -1118,11 +1169,24 @@ | ||
| 1118 | 1169 | $button.html('🔗 Connect to ' + getPluginName()); |
| 1119 | 1170 | } |
| 1120 | 1171 | } |
| 1121 | 1172 | |
| 1122 | - // Update header status indicator (only on General Settings page) | |
| 1123 | - if (isFullyConnected) { | |
| 1173 | + // Update header status indicator (only on General Settings page). | |
| 1174 | + // the header badge must reflect the confirmed heartbeat | |
| 1175 | + // round-trip (metaSync.is_connected), NOT mere API-key/UUID presence — | |
| 1176 | + // otherwise a revoked/invalid key still shows "Connected" because the | |
| 1177 | + // key string and UUID remain stored locally. | |
| 1178 | + // NOTE: wp_localize_script stringifies PHP booleans (true -> "1", | |
| 1179 | + // false -> ""), so normalise to a real boolean rather than ===. | |
| 1180 | + var heartbeatConnected = metaSync.is_connected === true | |
| 1181 | + || metaSync.is_connected === '1' | |
| 1182 | + || metaSync.is_connected === 1; | |
| 1183 | + if (isFullyConnected && heartbeatConnected) { | |
| 1124 | 1184 | updateHeaderStatus(true, 'Synced', getPluginName() + ' API key and ' + getOttoName() + ' UUID are configured'); |
| 1185 | + } else if (isFullyConnected) { | |
| 1186 | + // Key + UUID are configured but the heartbeat is not succeeding | |
| 1187 | + // (e.g. the key was revoked on the Search Atlas side). | |
| 1188 | + updateHeaderStatus(false, 'Not Connected', getPluginName() + ' API key is set but the heartbeat is not responding — the connection may be revoked or unreachable.'); | |
| 1125 | 1189 | } else if (hasApiKey && !hasOttoUuid) { |
| 1126 | 1190 | updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.'); |
| 1127 | 1191 | } else { |
| 1128 | 1192 | updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID'); |
| @@ -1128,11 +1192,11 @@ | ||
| 1128 | 1192 | updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID'); |
| 1129 | 1193 | } |
| 1130 | 1194 | } |
| 1131 | 1195 | |
| 1132 | - // Monitor API key field changes | |
| 1133 | - $('#searchatlas-api-key').on('input', updateConnectionStatus); | |
| 1134 | - | |
| 1196 | + // The API key is no longer manually editable (one-click auth only), so there | |
| 1197 | + // is no input event to monitor; status changes are driven by connect/disconnect. | |
| 1198 | + | |
| 1135 | 1199 | // Initial status update |
| 1136 | 1200 | updateConnectionStatus(); |
| 1137 | 1201 | |
| 1138 | 1202 | // Initialize dashboard iframe functionality |
| @@ -1141,13 +1205,12 @@ | ||
| 1141 | 1205 | // Settings dropdown now handled by inline script in HTML |
| 1142 | 1206 | |
| 1143 | 1207 | // Add debug function for connection status (accessible in console) |
| 1144 | 1208 | window.debugConnectionStatus = function () { |
| 1145 | - var apiKey = $('#searchatlas-api-key').val(); | |
| 1146 | - var hasApiKey = apiKey.trim() !== ''; | |
| 1147 | - | |
| 1209 | + var hasApiKey = searchAtlasKeyConfigured(); | |
| 1210 | + | |
| 1148 | 1211 | console.log('🔍 Connection Status Debug:', { |
| 1149 | - searchatlas_api_key: hasApiKey ? (apiKey.substring(0, 8) + '...') : 'EMPTY', | |
| 1212 | + searchatlas_credential_state: hasApiKey ? 'CONFIGURED (masked)' : 'EMPTY', | |
| 1150 | 1213 | otto_pixel_uuid: metaSync.otto_pixel_uuid || 'NOT SET', |
| 1151 | 1214 | connection_state: hasApiKey && metaSync.otto_pixel_uuid ? 'CONNECTED' : |
| 1152 | 1215 | hasApiKey ? 'PARTIAL (Missing ' + getOttoName() + ' UUID)' : 'NOT CONNECTED', |
| 1153 | 1216 | dashboard_tab_visible: hasApiKey && metaSync.otto_pixel_uuid ? 'YES' : 'NO', |
| @@ -1269,9 +1332,8 @@ | ||
| 1269 | 1332 | } |
| 1270 | 1333 | |
| 1271 | 1334 | var $resetButton = $('#reset-searchatlas-auth'); |
| 1272 | 1335 | var $connectButton = $('#connect-searchatlas-btn'); |
| 1273 | - var $apiKeyField = $('#searchatlas-api-key'); | |
| 1274 | 1336 | |
| 1275 | 1337 | // Show loading state (prevent dashboard.js conflicts) |
| 1276 | 1338 | $resetButton.prop('disabled', true) |
| 1277 | 1339 | .addClass('no-loading') // Prevent dashboard.js interference |
| @@ -1290,11 +1352,11 @@ | ||
| 1290 | 1352 | nonce: metaSync.reset_auth_nonce |
| 1291 | 1353 | }, |
| 1292 | 1354 | success: function (response) { |
| 1293 | 1355 | if (response.success) { |
| 1294 | - // Clear the API key field | |
| 1295 | - $apiKeyField.val(''); | |
| 1296 | - | |
| 1356 | + // Reset the masked display to the "Not configured" state | |
| 1357 | + setSearchAtlasMaskedDisplay(''); | |
| 1358 | + | |
| 1297 | 1359 | // Update button states |
| 1298 | 1360 | $connectButton.html('🔗 Connect to ' + getPluginName()); |
| 1299 | 1361 | $resetButton.remove(); // Remove reset button since no longer connected |
| 1300 | 1362 | |
| @@ -1389,9 +1451,9 @@ | ||
| 1389 | 1451 | /** |
| 1390 | 1452 | * Update UI elements when account is disconnected |
| 1391 | 1453 | */ |
| 1392 | 1454 | function updateUIForDisconnectedState() { |
| 1393 | - // WP-426: a cached manual-sync cooldown must not survive disconnect — | |
| 1455 | + // a cached manual-sync cooldown must not survive disconnect — | |
| 1394 | 1456 | // it keeps the Sync Now button locked after the user reconnects even |
| 1395 | 1457 | // though the server-side cooldown stamp was cleared with the auth data. |
| 1396 | 1458 | localStorage.removeItem('metasync_manual_sync_throttle_expires'); |
| 1397 | 1459 | refreshManualSyncCooldownUI(); |
| @@ -1397,9 +1459,10 @@ | ||
| 1397 | 1459 | refreshManualSyncCooldownUI(); |
| 1398 | 1460 | $('#sendAuthToken').prop('disabled', false).removeClass('is-throttled').text('🔄 Sync Now'); |
| 1399 | 1461 | |
| 1400 | 1462 | // Update API key field placeholder |
| 1401 | - $('#searchatlas-api-key').attr('placeholder', 'Your API key will appear here after authentication'); | |
| 1463 | + // Reset the masked API key display to the "Not configured" state | |
| 1464 | + setSearchAtlasMaskedDisplay(''); | |
| 1402 | 1465 | |
| 1403 | 1466 | // ✅ Clear OTTO Pixel UUID field |
| 1404 | 1467 | $('input[name="metasync_options[general][otto_pixel_uuid]"]').val(''); |
| 1405 | 1468 | |
| @@ -1449,14 +1512,13 @@ | ||
| 1449 | 1512 | /** |
| 1450 | 1513 | * Update UI elements when account is connected/authenticated |
| 1451 | 1514 | * Complementary function to updateUIForDisconnectedState() |
| 1452 | 1515 | */ |
| 1453 | - function updateUIForConnectedState(apiKey, ottoPixelUuid) { | |
| 1454 | - // Update API key field | |
| 1455 | - if (apiKey) { | |
| 1456 | - $('#searchatlas-api-key').val(apiKey); | |
| 1457 | - } | |
| 1458 | - | |
| 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 | + | |
| 1459 | 1521 | // ✅ Update OTTO Pixel UUID field |
| 1460 | 1522 | if (ottoPixelUuid) { |
| 1461 | 1523 | $('input[name="metasync_options[general][otto_pixel_uuid]"]').val(ottoPixelUuid); |
| 1462 | 1524 | } |
| @@ -2003,9 +2065,9 @@ | ||
| 2003 | 2065 | if (showAlerts) { |
| 2004 | 2066 | $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now'); |
| 2005 | 2067 | } |
| 2006 | 2068 | |
| 2007 | - if ($('#searchatlas-api-key') && $('#searchatlas-api-key').val() === '') { | |
| 2069 | + if ($('#searchatlas-api-key-display').length > 0 && !searchAtlasKeyConfigured()) { | |
| 2008 | 2070 | if (!is_hb) { |
| 2009 | 2071 | $('#sendAuthTokenTimestamp').html('Please save your ' + getPluginName() + ' API key'); |
| 2010 | 2072 | $('#sendAuthTokenTimestamp').css({ color: 'red' }); |
| 2011 | 2073 | |
| @@ -2127,9 +2189,10 @@ | ||
| 2127 | 2189 | url: ajaxurl, |
| 2128 | 2190 | type: 'GET', |
| 2129 | 2191 | data: { |
| 2130 | 2192 | action: 'metasync_clear_otto_cache', |
| 2131 | - clear_otto_cache: 1 | |
| 2193 | + clear_otto_cache: 1, | |
| 2194 | + nonce: metaSync.clear_otto_cache_nonce || '' | |
| 2132 | 2195 | }, |
| 2133 | 2196 | success: function (response) { |
| 2134 | 2197 | const now = new Date(); |
| 2135 | 2198 | $('#clear_otto_caches').text('Cache Cleared ' + now.toLocaleTimeString()); |
| @@ -2185,9 +2248,9 @@ | ||
| 2185 | 2248 | $btn.prop('disabled', false).removeClass('is-throttled').text('🔄 Sync Now'); |
| 2186 | 2249 | $('#sendAuthTokenTimestamp').text('Ready to sync').css({ color: 'green' }); |
| 2187 | 2250 | // Cooldown is over — clear the lingering "Request Throttled" admin |
| 2188 | 2251 | // notice at the top of the page. Without this the button resets to |
| 2189 | - // "Sync Now" but the red throttled bar stays visible (WP-350 QA). | |
| 2252 | + // "Sync Now" but the red throttled bar stays visible (QA). | |
| 2190 | 2253 | $('.metasync-sync-notice, .metasync-sync-error').remove(); |
| 2191 | 2254 | return; |
| 2192 | 2255 | } |
| 2193 | 2256 | remainingMinutes = Math.max(1, Math.ceil((storedExpiry - currentNowMs) / 60000)); |
| @@ -2262,9 +2325,9 @@ | ||
| 2262 | 2325 | data: formData + '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab), // Add the action and current tab |
| 2263 | 2326 | success: function (response) { |
| 2264 | 2327 | // Handle success response |
| 2265 | 2328 | if(response.success){ |
| 2266 | - // Per-field whitelabel validation issues (WP-413): the save succeeded for | |
| 2329 | + // Per-field whitelabel validation issues: the save succeeded for | |
| 2267 | 2330 | // other fields, so show a warning notice and stay on the page so the |
| 2268 | 2331 | // user can see which values were rejected. |
| 2269 | 2332 | const wlErrors = response.data && response.data.whitelabel_errors; |
| 2270 | 2333 | if (wlErrors && Object.keys(wlErrors).length > 0) { |
| @@ -2288,9 +2351,9 @@ | ||
| 2288 | 2351 | // add the tag query to the window location |
| 2289 | 2352 | let tabParam = new URLSearchParams(window.location.search).get('tab'); |
| 2290 | 2353 | let tabQuery = tabParam ? '&tab=' + encodeURIComponent(tabParam) : ''; |
| 2291 | 2354 | |
| 2292 | - // Prefer the server-sanitized slug (WP-413) so the redirect matches the page | |
| 2355 | + // Prefer the server-sanitized slug so the redirect matches the page | |
| 2293 | 2356 | // WordPress actually registered; fall back to the raw input field value. |
| 2294 | 2357 | const savedSlug = response.data && typeof response.data.saved_menu_slug === 'string' ? response.data.saved_menu_slug : null; |
| 2295 | 2358 | const inputSlug = (whiteLableUrl && whiteLableUrl !== '') ? whiteLableUrl : 'searchatlas'; |
| 2296 | 2359 | const rawSlug = (savedSlug !== null) ? (savedSlug || 'searchatlas') : inputSlug; |
| @@ -2610,11 +2673,12 @@ | ||
| 2610 | 2673 | } else if (response.data && response.data.api_key_validated === false) { |
| 2611 | 2674 | noticeType = 'notice-error'; |
| 2612 | 2675 | noticeMessage = $('<span/>').text(response.data.warning || 'The API key could not be verified. Your other settings were saved.').html(); |
| 2613 | 2676 | noAutoDismiss = true; |
| 2614 | - // Revert the input field to the previously saved valid key from the backend | |
| 2615 | - var revertKey = (response.data.previous_api_key !== undefined) ? response.data.previous_api_key : ''; | |
| 2616 | - $('#searchatlas-api-key').val(revertKey); | |
| 2677 | + // The server reverts the stored key to the last valid one on | |
| 2678 | + // verification failure, and the masked display is read-only, so | |
| 2679 | + // nothing in the DOM needs reverting here. The cleartext key is | |
| 2680 | + // never sent to the browser. | |
| 2617 | 2681 | } else if (response.data && response.data.api_key_removed === true) { |
| 2618 | 2682 | // API key was cleared — show warning, update status, page will reload |
| 2619 | 2683 | noticeType = 'notice-warning'; |
| 2620 | 2684 | noticeMessage = $('<span/>').text((response.data.warning || 'The API key has been removed.') + ' Reloading\u2026').html(); |
| @@ -3216,26 +3280,65 @@ | ||
| 3216 | 3280 | * @param {jQuery} $trigger - The trigger button |
| 3217 | 3281 | * @param {jQuery} $tooltip - The tooltip element |
| 3218 | 3282 | */ |
| 3219 | 3283 | function positionTooltip($trigger, $tooltip) { |
| 3220 | - // Skip positioning on mobile (uses fixed positioning) | |
| 3284 | + // Mobile uses the CSS fixed bottom-sheet layout; clear any inline desktop | |
| 3285 | + // styles so the stylesheet takes over. | |
| 3221 | 3286 | if (window.innerWidth <= 768) { |
| 3287 | + $tooltip.css({ position: '', left: '', top: '', right: '', bottom: '', transform: '', margin: '' }); | |
| 3222 | 3288 | return; |
| 3223 | 3289 | } |
| 3224 | 3290 | |
| 3225 | - var triggerRect = $trigger[0].getBoundingClientRect(); | |
| 3226 | - var tooltipWidth = $tooltip.outerWidth(); | |
| 3227 | - var viewportWidth = $(window).width(); | |
| 3228 | - var spaceRight = viewportWidth - triggerRect.right; | |
| 3291 | + // Move the tooltip up to the plugin wrapper so it escapes the | |
| 3292 | + // overflow:hidden on accordion sections / cards that was clipping it. | |
| 3293 | + // The wrapper carries data-theme (so the --dashboard-* CSS vars still | |
| 3294 | + // resolve) and is position:relative (not transformed), so the | |
| 3295 | + // position:fixed below stays relative to the viewport. Verified in a | |
| 3296 | + // headless-Chrome harness before shipping. | |
| 3297 | + var portal = document.querySelector('.metasync-dashboard-wrap') || document.body; | |
| 3298 | + if ($tooltip[0].parentNode !== portal) { | |
| 3299 | + portal.appendChild($tooltip[0]); | |
| 3300 | + } | |
| 3229 | 3301 | |
| 3230 | - // Check if tooltip would overflow on the right | |
| 3231 | - if (spaceRight < tooltipWidth + 20) { | |
| 3232 | - // Position on the left side | |
| 3233 | - $tooltip.attr('data-position', 'left'); | |
| 3234 | - } else { | |
| 3235 | - // Position on the right side (default) | |
| 3236 | - $tooltip.attr('data-position', 'right'); | |
| 3237 | - } | |
| 3302 | + var margin = 12; | |
| 3303 | + var rect = $trigger[0].getBoundingClientRect(); | |
| 3304 | + var vw = window.innerWidth; | |
| 3305 | + var vh = window.innerHeight; | |
| 3306 | + var tw = $tooltip.outerWidth(); | |
| 3307 | + var th = $tooltip.outerHeight(); | |
| 3308 | + | |
| 3309 | + // Horizontal: prefer to the right of the icon, else flip left; clamp to viewport. | |
| 3310 | + var left = rect.right + margin; | |
| 3311 | + if (left + tw + margin > vw) { left = rect.left - margin - tw; } | |
| 3312 | + if (left < margin) { left = margin; } | |
| 3313 | + if (left + tw + margin > vw) { left = Math.max(margin, vw - tw - margin); } | |
| 3314 | + | |
| 3315 | + // Vertical: center on the icon, then clamp so the top/bottom is never cut off. | |
| 3316 | + var top = rect.top + (rect.height / 2) - (th / 2); | |
| 3317 | + if (top < margin) { top = margin; } | |
| 3318 | + if (top + th + margin > vh) { top = Math.max(margin, vh - th - margin); } | |
| 3319 | + | |
| 3320 | + $tooltip.find('.metasync-tooltip-arrow').css('display', 'none'); | |
| 3321 | + // Place it, then play a small left-to-right slide-in. Position is | |
| 3322 | + // handled by left/top (fixed), so transform is free for the entrance | |
| 3323 | + // animation: start ~12px to the left and slide right into place. | |
| 3324 | + $tooltip.css({ | |
| 3325 | + position: 'fixed', | |
| 3326 | + left: Math.round(left) + 'px', | |
| 3327 | + top: Math.round(top) + 'px', | |
| 3328 | + right: 'auto', | |
| 3329 | + bottom: 'auto', | |
| 3330 | + margin: '0', | |
| 3331 | + transition: 'opacity 0.2s ease', | |
| 3332 | + transform: 'translateX(-12px)' | |
| 3333 | + }); | |
| 3334 | + // Force a reflow so the start position is committed before animating. | |
| 3335 | + void $tooltip[0].offsetWidth; | |
| 3336 | + $tooltip.css({ | |
| 3337 | + transition: 'opacity 0.2s ease, transform 0.2s ease', | |
| 3338 | + transform: 'translateX(0)' | |
| 3339 | + }); | |
| 3340 | + $tooltip.removeAttr('data-position'); | |
| 3238 | 3341 | } |
| 3239 | 3342 | |
| 3240 | 3343 | // Initialize tooltip system |
| 3241 | 3344 | initTooltipSystem(); |