diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php
index e84b3f293..71c075b2c 100644
--- a/application/views/bandmap/list.php
+++ b/application/views/bandmap/list.php
@@ -167,8 +167,8 @@
var lang_bandmap_mode = "= __("Mode"); ?>";
var lang_bandmap_band = "= __("Band"); ?>";
- // Enable ultra-compact radio status display for bandmap page (tooltip only)
- window.CAT_COMPACT_MODE = 'ultra-compact';
+ // Enable icon-only radio status display for bandmap page (just icon with tooltip)
+ window.CAT_COMPACT_MODE = 'icon-only';
// Map configuration (matches QSO map settings)
var map_tile_server = 'optionslib->get_option('option_map_tile_server');?>';
diff --git a/assets/js/cat.js b/assets/js/cat.js
index 95ec23bea..db8c44554 100644
--- a/assets/js/cat.js
+++ b/assets/js/cat.js
@@ -81,9 +81,37 @@ $(document).ready(function() {
};
// Global setting for radio status display mode (can be set by pages like bandmap)
- // Options: false (card wrapper), 'compact' (no card), 'ultra-compact' (tooltip only)
+ // Options: false (card wrapper), 'compact' (no card), 'ultra-compact' (icon+name+tooltip), 'icon-only' (icon+tooltip)
window.CAT_COMPACT_MODE = window.CAT_COMPACT_MODE || false;
+ /**
+ * Safely dispose of a Bootstrap tooltip without triggering _isWithActiveTrigger errors
+ * This works around a known Bootstrap bug where disposing during hide animation causes errors
+ * @param {Element} element - The DOM element with the tooltip
+ */
+ function safeDisposeTooltip(element) {
+ try {
+ var tooltipInstance = bootstrap.Tooltip.getInstance(element);
+ if (tooltipInstance) {
+ // Set _activeTrigger to empty object to prevent _isWithActiveTrigger error
+ if (tooltipInstance._activeTrigger) {
+ tooltipInstance._activeTrigger = {};
+ }
+ // Clear any pending timeouts
+ if (tooltipInstance._timeout) {
+ clearTimeout(tooltipInstance._timeout);
+ tooltipInstance._timeout = null;
+ }
+ // Dispose without calling hide first
+ tooltipInstance.dispose();
+ }
+ } catch(e) {
+ // Silently ignore any remaining errors
+ }
+ }
+ // Expose globally for other modules
+ window.safeDisposeTooltip = safeDisposeTooltip;
+
function initializeWebSocketConnection() {
try {
// Note: Browser will log WebSocket connection errors to console if server is unreachable
@@ -153,14 +181,19 @@ $(document).ready(function() {
if (typeof window.isCatTrackingEnabled !== 'undefined') {
if (!window.isCatTrackingEnabled) {
// CAT Control is OFF - show offline status and skip processing
- if (window.CAT_COMPACT_MODE === 'ultra-compact') {
+ if (window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') {
displayOfflineStatus('cat_disabled');
}
return;
}
}
- data.updated_minutes_ago = Math.floor((Date.now() - data.timestamp) / 60000);
+ // Calculate age from timestamp, defaulting to 0 (fresh) if timestamp is missing
+ if (data.timestamp) {
+ data.updated_minutes_ago = Math.floor((Date.now() - data.timestamp) / 60000);
+ } else {
+ data.updated_minutes_ago = 0; // Assume fresh if no timestamp
+ }
// Cache the radio data
updateCATui(data);
}
@@ -379,8 +412,8 @@ $(document).ready(function() {
* @param {string} reason - Optional reason: 'no_radio' (default) or 'cat_disabled'
*/
function displayOfflineStatus(reason) {
- // Display "Working offline" message with tooltip in ultra-compact mode
- if (window.CAT_COMPACT_MODE !== 'ultra-compact') {
+ // Display "Working offline" message with tooltip in ultra-compact/icon-only modes
+ if (window.CAT_COMPACT_MODE !== 'ultra-compact' && window.CAT_COMPACT_MODE !== 'icon-only') {
return;
}
@@ -390,11 +423,20 @@ $(document).ready(function() {
// Use translation variable if available, fallback to English
var offlineText = typeof lang_cat_working_offline !== 'undefined' ? lang_cat_working_offline : 'Working without CAT connection';
- const offlineHtml = '' +
- '' +
- '' + offlineText + '' +
- '' +
- '';
+ var offlineHtml;
+ if (window.CAT_COMPACT_MODE === 'icon-only') {
+ // Icon-only mode: just the icon with tooltip, styled as button for consistent height
+ offlineHtml = '' +
+ '' +
+ '';
+ } else {
+ // Ultra-compact mode: icon + text + info icon
+ offlineHtml = '' +
+ '' +
+ '' + offlineText + '' +
+ '' +
+ '';
+ }
let tooltipContent;
if (reason === 'cat_disabled') {
@@ -418,11 +460,15 @@ $(document).ready(function() {
// Initialize tooltip
var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
if (tooltipElement) {
- new bootstrap.Tooltip(tooltipElement, {
- title: tooltipContent,
- html: true,
- placement: 'bottom'
- });
+ try {
+ new bootstrap.Tooltip(tooltipElement, {
+ title: tooltipContent,
+ html: true,
+ placement: 'bottom'
+ });
+ } catch(e) {
+ // Ignore tooltip initialization errors
+ }
}
}
@@ -433,15 +479,16 @@ $(document).ready(function() {
* CAT_COMPACT_MODE options:
* false - Standard mode with card wrapper
* 'compact' - Compact mode without card wrapper
- * 'ultra-compact' - Ultra-compact mode showing only tooltip with info
+ * 'ultra-compact' - Ultra-compact mode showing icon, radio name, and tooltip
+ * 'icon-only' - Icon-only mode showing just icon with tooltip (for bandmap)
*/
function displayRadioStatus(state, data) {
// On bandmap page, only show radio status when CAT Control is enabled
if (typeof window.isCatTrackingEnabled !== 'undefined') {
if (!window.isCatTrackingEnabled) {
// CAT Control is OFF on bandmap
- // In ultra-compact mode, show "Working offline" with CAT disabled message
- if (window.CAT_COMPACT_MODE === 'ultra-compact') {
+ // In ultra-compact/icon-only mode, show "Working offline" with CAT disabled message
+ if (window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') {
// Check if a radio is selected
var selectedRadio = $('.radios option:selected').val();
if (selectedRadio && selectedRadio !== '0') {
@@ -548,7 +595,90 @@ $(document).ready(function() {
var html = baseStyle + icon + content + '';
// Update DOM based on global CAT_COMPACT_MODE setting
- if (window.CAT_COMPACT_MODE === 'ultra-compact') {
+ if (window.CAT_COMPACT_MODE === 'icon-only') {
+ // Icon-only mode: show just radio icon with tooltip containing all info
+ var tooltipContent = '';
+
+ if (state === 'success') {
+ var radioName = $('select.radios option:selected').text();
+ var connectionType = $(".radios option:selected").val() == 'ws' ? lang_cat_live : lang_cat_polling;
+ tooltipContent = '' + radioName + ' (' + connectionType + ')';
+
+ // Ensure frequency_formatted exists
+ var freqFormatted = data.frequency_formatted;
+ if (!freqFormatted || freqFormatted === 'undefined' || freqFormatted === 'nullkHz') {
+ freqFormatted = format_frequency(data.frequency);
+ }
+
+ // Add frequency info
+ if(data.frequency_rx && data.frequency_rx != 0 && data.frequency_rx !== 'undefined') {
+ // Split operation: show TX and RX separately
+ if (freqFormatted && freqFormatted !== 'undefined') {
+ tooltipContent += '
' + lang_cat_tx + ': ' + freqFormatted;
+ }
+ var rxFormatted = format_frequency(data.frequency_rx);
+ if (rxFormatted && rxFormatted !== 'undefined') {
+ tooltipContent += '
' + lang_cat_rx + ': ' + rxFormatted;
+ }
+ } else {
+ // Simplex operation: show TX/RX combined
+ if (freqFormatted && freqFormatted !== 'undefined') {
+ tooltipContent += '
' + lang_cat_tx_rx + ': ' + freqFormatted;
+ }
+ }
+
+ if(data.mode != null) {
+ tooltipContent += '
' + lang_cat_mode + ': ' + data.mode;
+ }
+ if(data.power != null && data.power != 0) {
+ tooltipContent += '
' + lang_cat_power + ': ' + data.power + 'W';
+ }
+ if ($(".radios option:selected").val() != 'ws') {
+ tooltipContent += '
' + lang_cat_polling_tooltip + '';
+ }
+ } else if (state === 'error') {
+ var radioName = typeof data === 'string' ? data : $('select.radios option:selected').text();
+ tooltipContent = lang_cat_connection_error + ': ' + radioName + '
' + lang_cat_connection_lost;
+ } else if (state === 'timeout') {
+ var radioName = typeof data === 'string' ? data : $('select.radios option:selected').text();
+ tooltipContent = lang_cat_connection_timeout + ': ' + radioName + '
' + lang_cat_data_stale;
+ } else if (state === 'not_logged_in') {
+ tooltipContent = lang_cat_not_logged_in;
+ }
+
+ var iconOnlyHtml = '' +
+ '' +
+ '';
+
+ if (!$('#radio_cat_state').length) {
+ $('#radio_status').append(iconOnlyHtml);
+ } else {
+ $('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
+ safeDisposeTooltip(this);
+ });
+ $('#radio_cat_state').replaceWith(iconOnlyHtml);
+ }
+
+ var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
+ if (tooltipElement) {
+ try {
+ new bootstrap.Tooltip(tooltipElement, {
+ title: tooltipContent,
+ html: true,
+ placement: 'bottom'
+ });
+ } catch(e) {
+ // Ignore tooltip initialization errors
+ }
+ }
+
+ // Add blink animation on update
+ $('#radio_status .fa-radio').addClass('blink-once');
+ setTimeout(function() {
+ $('#radio_status .fa-radio').removeClass('blink-once');
+ }, 600);
+
+ } else if (window.CAT_COMPACT_MODE === 'ultra-compact') {
// Ultra-compact mode: show radio icon, radio name, and question mark with tooltip
var tooltipContent = '';
var radioName = '';
@@ -562,17 +692,30 @@ $(document).ready(function() {
connectionType = lang_cat_live;
} else {
connectionType = lang_cat_polling;
- } tooltipContent = '' + radioName + ' (' + connectionType + ')';
+ }
+ tooltipContent = '' + radioName + ' (' + connectionType + ')';
+
+ // Ensure frequency_formatted exists
+ var freqFormatted = data.frequency_formatted;
+ if (!freqFormatted || freqFormatted === 'undefined' || freqFormatted === 'nullkHz') {
+ freqFormatted = format_frequency(data.frequency);
+ }
// Add frequency info
- if(data.frequency_rx != null && data.frequency_rx != 0) {
- tooltipContent += '
' + lang_cat_tx + ': ' + data.frequency_formatted;
- data.frequency_rx_formatted = format_frequency(data.frequency_rx);
- if (data.frequency_rx_formatted) {
- tooltipContent += '
' + lang_cat_rx + ': ' + data.frequency_rx_formatted;
+ if(data.frequency_rx && data.frequency_rx != 0 && data.frequency_rx !== 'undefined') {
+ // Split operation: show TX and RX separately
+ if (freqFormatted && freqFormatted !== 'undefined') {
+ tooltipContent += '
' + lang_cat_tx + ': ' + freqFormatted;
+ }
+ var rxFormatted = format_frequency(data.frequency_rx);
+ if (rxFormatted && rxFormatted !== 'undefined') {
+ tooltipContent += '
' + lang_cat_rx + ': ' + rxFormatted;
}
} else {
- tooltipContent += '
' + lang_cat_tx_rx + ': ' + data.frequency_formatted;
+ // Simplex operation: show TX/RX combined
+ if (freqFormatted && freqFormatted !== 'undefined') {
+ tooltipContent += '
' + lang_cat_tx_rx + ': ' + freqFormatted;
+ }
}
// Add mode
@@ -611,10 +754,7 @@ $(document).ready(function() {
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
- var tooltipInstance = bootstrap.Tooltip.getInstance(this);
- if (tooltipInstance) {
- tooltipInstance.dispose();
- }
+ safeDisposeTooltip(this);
});
$('#radio_cat_state').replaceWith(ultraCompactHtml);
}
@@ -622,11 +762,15 @@ $(document).ready(function() {
// Initialize tooltip with dynamic content
var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
if (tooltipElement) {
- new bootstrap.Tooltip(tooltipElement, {
- title: tooltipContent,
- html: true,
- placement: 'bottom'
- });
+ try {
+ new bootstrap.Tooltip(tooltipElement, {
+ title: tooltipContent,
+ html: true,
+ placement: 'bottom'
+ });
+ } catch(e) {
+ // Ignore tooltip initialization errors
+ }
}
// Add blink animation to radio icon on update
@@ -643,10 +787,7 @@ $(document).ready(function() {
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
- var tooltipInstance = bootstrap.Tooltip.getInstance(this);
- if (tooltipInstance) {
- tooltipInstance.dispose();
- }
+ safeDisposeTooltip(this);
});
$('#radio_cat_state').html(html);
}
@@ -658,18 +799,15 @@ $(document).ready(function() {
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
- var tooltipInstance = bootstrap.Tooltip.getInstance(this);
- if (tooltipInstance) {
- tooltipInstance.dispose();
- }
+ safeDisposeTooltip(this);
});
// Update existing panel content
$('#radio_cat_state .card-body').html(html);
}
}
- // Initialize Bootstrap tooltips for any new tooltip elements in the radio panel (except ultra-compact which handles its own)
- if (window.CAT_COMPACT_MODE !== 'ultra-compact') {
+ // Initialize Bootstrap tooltips for any new tooltip elements in the radio panel (except ultra-compact/icon-only which handle their own)
+ if (window.CAT_COMPACT_MODE !== 'ultra-compact' && window.CAT_COMPACT_MODE !== 'icon-only') {
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
new bootstrap.Tooltip(this);
});
@@ -1007,8 +1145,8 @@ $(document).ready(function() {
$('#toggleCatTracking').prop('disabled', false).removeClass('disabled');
// Always initialize WebSocket connection
initializeWebSocketConnection();
- // In ultra-compact mode, show offline status if CAT Control is disabled
- if (window.CAT_COMPACT_MODE === 'ultra-compact' && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
+ // In ultra-compact/icon-only mode, show offline status if CAT Control is disabled
+ if ((window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
displayOfflineStatus('cat_disabled');
}
} else {
@@ -1020,8 +1158,8 @@ $(document).ready(function() {
$('#toggleCatTracking').prop('disabled', false).removeClass('disabled');
// Always start polling
CATInterval=setInterval(updateFromCAT, CAT_CONFIG.POLL_INTERVAL);
- // In ultra-compact mode, show offline status if CAT Control is disabled
- if (window.CAT_COMPACT_MODE === 'ultra-compact' && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
+ // In ultra-compact/icon-only mode, show offline status if CAT Control is disabled
+ if ((window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
displayOfflineStatus('cat_disabled');
}
}
diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js
index d96e39937..217708ae7 100644
--- a/assets/js/sections/bandmap_list.js
+++ b/assets/js/sections/bandmap_list.js
@@ -162,11 +162,18 @@ $(function() {
$('.spottable [data-bs-toggle="tooltip"]').each(function() {
try {
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
- const tooltipInstance = bootstrap.Tooltip.getInstance(this);
- if (tooltipInstance) {
- // Hide first, then dispose - prevents _isWithActiveTrigger errors
- try { tooltipInstance.hide(); } catch(e) {}
- try { tooltipInstance.dispose(); } catch(e) {}
+ // Use safeDisposeTooltip if available (from cat.js), otherwise dispose directly
+ if (typeof window.safeDisposeTooltip === 'function') {
+ window.safeDisposeTooltip(this);
+ } else {
+ const tooltipInstance = bootstrap.Tooltip.getInstance(this);
+ if (tooltipInstance) {
+ // Set _activeTrigger to empty object to prevent _isWithActiveTrigger error
+ if (tooltipInstance._activeTrigger) {
+ tooltipInstance._activeTrigger = {};
+ }
+ try { tooltipInstance.dispose(); } catch(e) {}
+ }
}
}
} catch (err) {
@@ -1385,10 +1392,16 @@ $(function() {
try {
// Dispose existing tooltip instance if it exists
- const existingTooltip = bootstrap.Tooltip.getInstance(this);
- if (existingTooltip) {
- try { existingTooltip.hide(); } catch(e) {}
- try { existingTooltip.dispose(); } catch(e) {}
+ if (typeof window.safeDisposeTooltip === 'function') {
+ window.safeDisposeTooltip(this);
+ } else {
+ const existingTooltip = bootstrap.Tooltip.getInstance(this);
+ if (existingTooltip) {
+ if (existingTooltip._activeTrigger) {
+ existingTooltip._activeTrigger = {};
+ }
+ try { existingTooltip.dispose(); } catch(e) {}
+ }
}
// Create new tooltip instance with proper configuration
@@ -2785,8 +2798,17 @@ $(function() {
}
// Display radio status when CAT is enabled (don't call full catJsUpdateCATui as it updates QSO form fields)
+ // But we need to check for stale data like cat.js does
if (isCatTrackingEnabled && typeof window.displayRadioStatus === 'function') {
- window.displayRadioStatus('success', data);
+ // Check if data is too old (same logic as cat.js updateCATui)
+ var minutes = typeof cat_timeout_minutes !== 'undefined' ? cat_timeout_minutes : 5;
+ if (data.updated_minutes_ago > minutes) {
+ // Data is stale - show timeout
+ var radioName = $('select.radios option:selected').text();
+ window.displayRadioStatus('timeout', radioName);
+ } else {
+ window.displayRadioStatus('success', data);
+ }
}
// Update frequency gradient colors for all visible rows (works in both normal and purple CAT modes)
@@ -3685,9 +3707,17 @@ $(function() {
window.isCatTrackingEnabled = true;
catState = 'on';
- // Display last known data if available
+ // Display last known data if available and not stale
if (window.lastCATData && typeof window.displayRadioStatus === 'function') {
- window.displayRadioStatus('success', window.lastCATData);
+ // Check if data is still fresh (use same timeout as cat.js)
+ var minutes = typeof cat_timeout_minutes !== 'undefined' ? cat_timeout_minutes : 5;
+ if (window.lastCATData.updated_minutes_ago <= minutes) {
+ window.displayRadioStatus('success', window.lastCATData);
+ } else {
+ // Data is stale - show timeout
+ var radioName = $('select.radios option:selected').text();
+ window.displayRadioStatus('timeout', radioName);
+ }
}
// Trigger immediate polling update if using polling radio