From adeccc24fb596306893840a26a545415566c2eb6 Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Wed, 15 Oct 2025 22:01:03 +0530 Subject: [PATCH] up --- frontend/public/spot-worker.js | 140 +++++++++++++ frontend/src/App.svelte | 94 ++++++++- frontend/src/components/FilterBar.svelte | 19 +- frontend/src/components/Header.svelte | 11 ++ frontend/src/components/SpotsTable.svelte | 99 +++++----- frontend/src/components/Toast.svelte | 6 +- frontend/src/lib/spotCache.js | 229 ++++++++++++++++++++++ frontend/src/lib/spotWorker.js | 92 +++++++++ watchlist.json | 2 +- 9 files changed, 631 insertions(+), 61 deletions(-) create mode 100644 frontend/public/spot-worker.js create mode 100644 frontend/src/lib/spotCache.js create mode 100644 frontend/src/lib/spotWorker.js diff --git a/frontend/public/spot-worker.js b/frontend/public/spot-worker.js new file mode 100644 index 0000000..dc2dd23 --- /dev/null +++ b/frontend/public/spot-worker.js @@ -0,0 +1,140 @@ +// spot-worker.js - Web Worker pour traiter les spots + +self.onmessage = function(e) { + const { type, data } = e.data; + + switch(type) { + case 'FILTER_SPOTS': + const filtered = filterSpots(data.spots, data.filters, data.watchlist); + self.postMessage({ type: 'FILTERED_SPOTS', data: filtered }); + break; + + case 'SORT_SPOTS': + const sorted = sortSpots(data.spots, data.sortBy, data.sortOrder); + self.postMessage({ type: 'SORTED_SPOTS', data: sorted }); + break; + + default: + console.error('Unknown worker message type:', type); + } +}; + +function filterSpots(allSpots, filters, watchlist) { + if (!allSpots || !Array.isArray(allSpots)) return []; + + // Si "All" est actif, retourner tous les spots + if (filters.showAll) { + return allSpots; + } + + const bandFiltersActive = filters.band160M || filters.band80M || filters.band60M || + filters.band40M || filters.band30M || filters.band20M || filters.band17M || + filters.band15M || filters.band12M || filters.band10M || filters.band6M; + + const typeFiltersActive = filters.showNewDXCC || filters.showNewBand || + filters.showNewMode || filters.showNewBandMode || filters.showNewSlot || + filters.showWorked || filters.showWatchlist; + + const modeFiltersActive = filters.showDigital || filters.showSSB || filters.showCW; + + return allSpots.filter(spot => { + let matchesBand = false; + let matchesType = false; + let matchesMode = false; + + // Filtres de bande + if (bandFiltersActive) { + matchesBand = ( + (filters.band160M && spot.Band === '160M') || + (filters.band80M && spot.Band === '80M') || + (filters.band60M && spot.Band === '60M') || + (filters.band40M && spot.Band === '40M') || + (filters.band30M && spot.Band === '30M') || + (filters.band20M && spot.Band === '20M') || + (filters.band17M && spot.Band === '17M') || + (filters.band15M && spot.Band === '15M') || + (filters.band12M && spot.Band === '12M') || + (filters.band10M && spot.Band === '10M') || + (filters.band6M && spot.Band === '6M') + ); + } + + // Filtres de type + if (typeFiltersActive) { + if (filters.showWatchlist) { + const inWatchlist = watchlist.some(pattern => + spot.DX === pattern || spot.DX.startsWith(pattern) + ); + if (inWatchlist) matchesType = true; + } + if (filters.showNewDXCC && spot.NewDXCC) matchesType = true; + else if (filters.showNewBandMode && spot.NewBand && spot.NewMode && !spot.NewDXCC) matchesType = true; + else if (filters.showNewBand && spot.NewBand && !spot.NewMode && !spot.NewDXCC) matchesType = true; + else if (filters.showNewMode && spot.NewMode && !spot.NewBand && !spot.NewDXCC) matchesType = true; + else if (filters.showNewSlot && spot.NewSlot && !spot.NewDXCC && !spot.NewBand && !spot.NewMode) matchesType = true; + else if (filters.showWorked && spot.Worked) matchesType = true; + } + + // Filtres de mode + if (modeFiltersActive) { + const mode = spot.Mode || ''; + if (filters.showDigital && ['FT8', 'FT4', 'RTTY'].includes(mode)) matchesMode = true; + if (filters.showSSB && ['SSB', 'USB', 'LSB'].includes(mode)) matchesMode = true; + if (filters.showCW && mode === 'CW') matchesMode = true; + } + + // Logique de combinaison des filtres + const numActiveFilterTypes = [bandFiltersActive, typeFiltersActive, modeFiltersActive].filter(Boolean).length; + + if (numActiveFilterTypes === 0) return false; + if (numActiveFilterTypes === 1) { + if (bandFiltersActive) return matchesBand; + if (typeFiltersActive) return matchesType; + if (modeFiltersActive) return matchesMode; + } + if (numActiveFilterTypes === 2) { + if (bandFiltersActive && typeFiltersActive) return matchesBand && matchesType; + if (bandFiltersActive && modeFiltersActive) return matchesBand && matchesMode; + if (typeFiltersActive && modeFiltersActive) return matchesType && matchesMode; + } + if (numActiveFilterTypes === 3) { + return matchesBand && matchesType && matchesMode; + } + + return false; + }); +} + +function sortSpots(spots, sortBy, sortOrder) { + if (!spots || !Array.isArray(spots)) return []; + + const sorted = [...spots].sort((a, b) => { + let compareValue = 0; + + switch(sortBy) { + case 'dx': + compareValue = a.DX.localeCompare(b.DX); + break; + case 'frequency': + compareValue = parseFloat(a.FrequencyMhz) - parseFloat(b.FrequencyMhz); + break; + case 'band': + compareValue = a.Band.localeCompare(b.Band); + break; + case 'mode': + compareValue = a.Mode.localeCompare(b.Mode); + break; + case 'time': + compareValue = a.UTCTime.localeCompare(b.UTCTime); + break; + default: + compareValue = a.ID - b.ID; // Par défaut, tri par ID + } + + return sortOrder === 'asc' ? compareValue : -compareValue; + }); + + return sorted; +} + +console.log('Spot Worker initialized'); \ No newline at end of file diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 8392374..9e296ef 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -7,6 +7,9 @@ import Sidebar from './components/Sidebar.svelte'; import Toast from './components/Toast.svelte'; import ErrorBanner from './components/ErrorBanner.svelte'; + import { spotWorker } from './lib/spotWorker.js'; + import { spotCache } from './lib/spotCache.js'; + // State let spots = []; @@ -68,6 +71,9 @@ let maxReconnectAttempts = 10; let isShuttingDown = false; let filterTimeout; + let isFiltering = false; + let cacheLoaded = false; + let notifiedSpots = new Set(); $: { if (spotFilters.showAll) { @@ -75,8 +81,10 @@ } else { if (filterTimeout) clearTimeout(filterTimeout); - filterTimeout = setTimeout(() => { - filteredSpots = applyFilters(spots, spotFilters, watchlist); + filterTimeout = setTimeout(async () => { + isFiltering = true; + filteredSpots = await spotWorker.filterSpots(spots, spotFilters, watchlist); + isFiltering = false; }, 150); } } @@ -248,29 +256,57 @@ errorMessage = 'Unable to connect to server. Please refresh the page.'; } }; - } catch (error) { // ✅ AJOUTER cette ligne + } catch (error) { console.error('Error creating WebSocket:', error); wsStatus = 'disconnected'; - } // ✅ AJOUTER cette ligne + } } function handleWebSocketMessage(message) { switch (message.type) { case 'stats': stats = message.data; + spotCache.saveMetadata('stats', stats).catch(err => console.error('Cache save error:', err)); break; case 'spots': - spots = message.data || []; + const newSpots = message.data || []; + + if (stats.myCallsign && newSpots.length > 0) { + newSpots.forEach(spot => { + // Vérifier si c'est votre callsign ET qu'on ne l'a pas déjà notifié + if (spot.DX === stats.myCallsign && !notifiedSpots.has(spot.ID)) { + notifiedSpots.add(spot.ID); + showToast( + `📢 You were spotted by ${spot.SpotterCallsign} on ${spot.FrequencyMhz} (${spot.Band} ${spot.Mode})`, + 'mycall' + ); + } + }); + + if (notifiedSpots.size > 100) { + const arr = Array.from(notifiedSpots); + notifiedSpots = new Set(arr.slice(-100)); + } + } + + spots = newSpots; + + if (spots.length > 0) { + spotCache.saveSpots(spots).catch(err => console.error('Cache save error:', err)); + } break; case 'spotters': topSpotters = message.data || []; break; case 'watchlist': - // ✅ La watchlist est mise à jour par WebSocket watchlist = message.data || []; + spotCache.saveMetadata('watchlist', watchlist).catch(err => console.error('Cache save error:', err)); break; case 'log': recentQSOs = message.data || []; + if (recentQSOs.length > 0) { + spotCache.saveQSOs(recentQSOs).catch(err => console.error('Cache save error:', err)); + } break; case 'logStats': logStats = message.data || {}; @@ -291,7 +327,7 @@ toastType = type; setTimeout(() => { toastMessage = ''; - }, 3000); + }, 5000); } async function fetchSolarData() { @@ -401,8 +437,44 @@ async function shutdownApp() { } } - onMount(() => { - const savedSoundEnabled = localStorage.getItem('soundEnabled'); + onMount(async () => { + // ✅ Initialiser IndexedDB + try { + await spotCache.init(); + + // ✅ Charger les données du cache immédiatement + const cachedSpots = await spotCache.getSpots(); + if (cachedSpots.length > 0) { + spots = cachedSpots; + cacheLoaded = true; + console.log('📦 Loaded data from cache'); + } + + // Charger watchlist du cache + const cachedWatchlist = await spotCache.getMetadata('watchlist'); + if (cachedWatchlist) { + watchlist = cachedWatchlist; + } + + // Charger QSOs du cache + const cachedQSOs = await spotCache.getQSOs(); + if (cachedQSOs.length > 0) { + recentQSOs = cachedQSOs; + } + + // Charger stats du cache + const cachedStats = await spotCache.getMetadata('stats'); + if (cachedStats) { + stats = cachedStats; + } + } catch (error) { + console.error('Failed to initialize cache:', error); + } + + // Initialiser le worker + spotWorker.init(); + + // Se connecter au WebSocket (qui va mettre à jour avec les données fraîches) connectWebSocket(); fetchSolarData(); @@ -414,6 +486,8 @@ async function shutdownApp() { window.addEventListener('sendSpot', handleSendSpot); return () => { + spotWorker.terminate(); + spotCache.close(); if (ws) ws.close(); if (reconnectTimer) clearTimeout(reconnectTimer); clearInterval(solarInterval); @@ -436,6 +510,7 @@ async function shutdownApp() { {stats} {solarData} {wsStatus} + {cacheLoaded} on:shutdown={shutdownApp} /> @@ -448,6 +523,7 @@ async function shutdownApp() { {spotFilters} {spots} {watchlist} + {isFiltering} on:toggleFilter={(e) => toggleFilter(e.detail)} /> diff --git a/frontend/src/components/FilterBar.svelte b/frontend/src/components/FilterBar.svelte index 902f8c7..1354cb2 100644 --- a/frontend/src/components/FilterBar.svelte +++ b/frontend/src/components/FilterBar.svelte @@ -4,6 +4,8 @@ export let spotFilters; export let spots; export let watchlist; + export let isFiltering = false; + const dispatch = createEventDispatcher(); @@ -76,9 +78,20 @@ } -
-
- TYPE: +
+
+ + {#if isFiltering} +
+ + + + + Filtering... +
+ {/if} + + TYPE: