From 0924e1987fe3e89a2c740bf3c582233990609de9 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sun, 2 Nov 2025 22:28:28 +0100 Subject: [PATCH] Fresh/Obsolete spots --- assets/css/bandmap_list.css | 32 ++++++++ assets/js/sections/bandmap_list.js | 128 ++++++++++++++++++++++++++--- 2 files changed, 150 insertions(+), 10 deletions(-) diff --git a/assets/css/bandmap_list.css b/assets/css/bandmap_list.css index 1aef6956b..c4f89c574 100644 --- a/assets/css/bandmap_list.css +++ b/assets/css/bandmap_list.css @@ -83,6 +83,38 @@ html { --bs-table-accent-bg: #3981b2; } +/* TTL-based spot styling - Expiring spots (TTL=0) */ +.spot-expiring { + transition: all 300ms ease; + --bs-table-bg: rgba(220, 53, 69, 0.25) !important; /* Bootstrap danger color, subtle */ + --bs-table-accent-bg: rgba(220, 53, 69, 0.25) !important; + background-color: rgba(220, 53, 69, 0.25) !important; +} + +/* Very new spots (< 1 minute old) */ +.spot-very-new { + transition: all 300ms ease; + --bs-table-bg: rgba(25, 135, 84, 0.2) !important; /* Bootstrap success color, light */ + --bs-table-accent-bg: rgba(25, 135, 84, 0.2) !important; + background-color: rgba(25, 135, 84, 0.2) !important; +} + +/* Hover effect for all spot rows */ +.spottable tbody tr:hover { + --bs-table-bg: rgba(13, 110, 253, 0.15) !important; /* Bootstrap primary blue */ + --bs-table-accent-bg: rgba(13, 110, 253, 0.15) !important; + background-color: rgba(13, 110, 253, 0.15) !important; + transition: all 150ms ease; +} + +/* Don't apply hover to empty/loading rows */ +.spottable tbody tr.dataTables_empty:hover, +.spottable tbody tr.dataTables_empty:hover td { + --bs-table-bg: transparent !important; + --bs-table-accent-bg: transparent !important; + background-color: transparent !important; +} + tbody a { color: inherit; text-decoration: none; diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index e1c3cbbd9..72c2b09a0 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -27,10 +27,16 @@ * - Activity flags (POTA, SOTA, WWFF, IOTA, Contest) * - Auto-refresh with 60-second countdown timer * - DXCC status color coding (Confirmed/Worked/New) + * - TTL-based spot lifecycle (expiring spots shown in red) */ 'use strict'; +// ======================================== +// CONFIGURATION +// ======================================== +const SPOT_REFRESH_INTERVAL = 60; // Auto-refresh interval in seconds + $(function() { // ======================================== @@ -469,8 +475,17 @@ $(function() { timestamp: null }; + // TTL (Time To Live) management for spots + // Key: "callsign_frequency_spotter", Value: TTL count + var spotTTLMap = new Map(); + + // Generate unique key for spot identification + function getSpotKey(spot) { + return spot.spotted + '_' + spot.frequency + '_' + spot.spotter; + } + // Auto-refresh timer state - var refreshCountdown = 60; + var refreshCountdown = SPOT_REFRESH_INTERVAL; var refreshTimerInterval = null; // ======================================== @@ -580,7 +595,7 @@ $(function() { clearInterval(refreshTimerInterval); } - refreshCountdown = 60; + refreshCountdown = SPOT_REFRESH_INTERVAL; refreshTimerInterval = setInterval(function() { refreshCountdown--; @@ -589,7 +604,7 @@ $(function() { let table = get_dtable(); table.clear(); fill_list(currentFilters.deContinent, dxcluster_maxage); - refreshCountdown = 60; + refreshCountdown = SPOT_REFRESH_INTERVAL; } else { if (!isFetchInProgress && lastFetchParams.timestamp !== null) { $('#refreshIcon').removeClass('fa-spinner fa-spin').addClass('fa-hourglass-half'); @@ -709,6 +724,20 @@ $(function() { let spots2render = 0; cachedSpotData.forEach((single) => { + // Check TTL - skip spots with TTL < 0 (completely hidden) + let spotKey = getSpotKey(single); + let ttl = spotTTLMap.get(spotKey); + + // Skip if TTL is undefined or < 0 + if (ttl === undefined || ttl < 0) { + return; + } + + // Debug: Log TTL for first few spots + if (spots2render < 3) { + console.log('Spot:', single.spotted, 'Freq:', single.frequency, 'TTL:', ttl); + } + // Extract time from spot data - use 'when' field let timeOnly = single.when; @@ -984,8 +1013,20 @@ $(function() { data[0].push(flags_column); // Message column data[0].push(single.message || ''); - // Add row to table (with "fresh" class for new spots animation) - if (oldtable.length > 0) { + // Add row to table with appropriate styling based on TTL and age + // Priority: TTL=0 (expiring) > age < 1 min (very new) > fresh + let rowClass = ''; + let ageMinutesForStyling = single.age || 0; + + if (ttl === 0) { + // Expiring spot (gone from cluster but visible for one more cycle) + rowClass = 'spot-expiring'; + console.log('EXPIRING SPOT:', single.spotted, 'Freq:', single.frequency, 'TTL:', ttl); + } else if (ageMinutesForStyling < 1) { + // Very new spot (less than 1 minute old) + rowClass = 'spot-very-new'; + } else if (oldtable.length > 0) { + // Check if this is a new spot (not in old table) let update = false; oldtable.each(function (srow) { if (JSON.stringify(srow) === JSON.stringify(data[0])) { @@ -993,9 +1034,16 @@ $(function() { } }); if (!update) { - table.rows.add(data).draw().nodes().to$().addClass("fresh"); - } else { - table.rows.add(data).draw(); + rowClass = 'fresh'; // Fresh spot animation + } + } + + // Add row with appropriate class + if (rowClass) { + let addedRow = table.rows.add(data).draw().nodes().to$(); + addedRow.addClass(rowClass); + if (ttl === 0) { + console.log('Added expiring class to row:', addedRow.hasClass('spot-expiring')); } } else { table.rows.add(data).draw(); @@ -1395,7 +1443,67 @@ $(function() { if (dxspots.length > 0) { dxspots.sort(SortByQrg); // Sort by frequency - cachedSpotData = dxspots; + + // TTL Management: Process new spots and update TTL values + let newSpotKeys = new Set(); + + // First pass: identify all spots in the new data + dxspots.forEach(function(spot) { + let key = getSpotKey(spot); + newSpotKeys.add(key); + }); + + // Second pass: Update TTL for all existing spots + // - Decrement all TTL values by 1 + // - If spot exists in new data, set TTL back to 1 (stays valid) + // - Remove spots with TTL < -1 + let ttlStats = { stillValid: 0, expiring: 0, removed: 0, added: 0 }; + let expiringSpots = []; // Store spots with TTL=0 that need to be shown + + for (let [key, ttl] of spotTTLMap.entries()) { + let newTTL = ttl - 1; // Decrement all spots + + if (newSpotKeys.has(key)) { + newTTL = 1; // Reset to 1 if spot still exists (keeps it valid) + ttlStats.stillValid++; + } else { + if (newTTL === 0) { + ttlStats.expiring++; + // Find the spot in previous cachedSpotData to keep it for display + if (cachedSpotData) { + let expiringSpot = cachedSpotData.find(s => getSpotKey(s) === key); + if (expiringSpot) { + expiringSpots.push(expiringSpot); + } + } + } + } + + if (newTTL < -1) { + spotTTLMap.delete(key); // Remove completely hidden spots + ttlStats.removed++; + } else { + spotTTLMap.set(key, newTTL); + } + } + + // Third pass: Add new spots that weren't in the map + dxspots.forEach(function(spot) { + let key = getSpotKey(spot); + if (!spotTTLMap.has(key)) { + spotTTLMap.set(key, 1); // New spot starts with TTL = 1 + ttlStats.added++; + } + }); + + console.log('TTL Update:', ttlStats, 'Total tracked spots:', spotTTLMap.size); + if (expiringSpots.length > 0) { + console.log('Adding', expiringSpots.length, 'expiring spots back to display'); + } + + // Merge new spots with expiring spots (TTL=0) for display + cachedSpotData = dxspots.concat(expiringSpots); + cachedSpotData.sort(SortByQrg); // Re-sort after merging } else { cachedSpotData = []; } @@ -1404,7 +1512,7 @@ $(function() { isFetchInProgress = false; renderFilteredSpots(); // Apply client-side filters and render - startRefreshTimer(); // Start 60s countdown + startRefreshTimer(); // Start 10s countdown - TEMPORARY }).fail(function(jqXHR, textStatus) { currentAjaxRequest = null;