This commit is contained in:
2025-10-15 22:01:03 +05:30
parent 33ae9aebad
commit adeccc24fb
9 changed files with 631 additions and 61 deletions

View File

@@ -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');

View File

@@ -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)}
/>

View File

@@ -4,6 +4,8 @@
export let spotFilters;
export let spots;
export let watchlist;
export let isFiltering = false;
const dispatch = createEventDispatcher();
@@ -76,9 +78,20 @@
}
</script>
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-2 border border-slate-700/50 mb-3">
<div class="flex items-center gap-1 flex-wrap">
<span class="text-xs font-bold text-slate-400 mr-2">TYPE:</span>
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-2 border border-slate-700/50 mb-3">
<div class="flex items-center gap-1 flex-wrap">
<!-- ✅ AJOUTER le spinner ici -->
{#if isFiltering}
<div class="inline-flex items-center gap-2 text-xs text-blue-400 mr-3">
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Filtering...
</div>
{/if}
<span class="text-xs font-bold text-slate-400 mr-2">TYPE:</span>
<button
on:click={() => dispatch('toggleFilter', 'showAll')}

View File

@@ -4,6 +4,8 @@
export let stats;
export let solarData;
export let wsStatus;
export let cacheLoaded = false;
const dispatch = createEventDispatcher();
@@ -100,6 +102,15 @@
Flex
</span>
{#if cacheLoaded}
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semibold bg-purple-500/20 text-purple-400">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>
</svg>
Cached
</span>
{/if}
<button
on:click={() => dispatch('shutdown')}
class="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors flex items-center gap-1">

View File

@@ -1,5 +1,6 @@
<script>
import { createEventDispatcher } from 'svelte';
import VirtualList from 'svelte-virtual-list';
export let spots;
export let watchlist;
@@ -7,6 +8,9 @@
const dispatch = createEventDispatcher();
let container;
const itemHeight = 45;
function handleSpotClick(spot) {
dispatch('clickSpot', {
callsign: spot.DX,
@@ -52,51 +56,54 @@
<h2 class="text-lg font-bold">Recent Spots (<span>{spots.length}</span>)</h2>
</div>
<div class="overflow-y-auto flex-1">
<table class="w-full text-sm">
<thead class="bg-slate-900/50 sticky top-0">
<tr class="text-left text-xs text-slate-400">
<th class="p-2 font-semibold">DX</th>
<th class="p-2 font-semibold">Freq</th>
<th class="p-2 font-semibold">Band</th>
<th class="p-2 font-semibold">Mode</th>
<th class="p-2 font-semibold">Spotter</th>
<th class="p-2 font-semibold">Time</th>
<th class="p-2 font-semibold">Country</th>
<th class="p-2 font-semibold">Status</th>
</tr>
</thead>
<tbody>
{#each spots as spot (spot.ID)}
<tr class="border-b border-slate-700/30 hover:bg-slate-700/30 transition-colors text-sm">
<td class="p-2">
<button
class="font-bold text-blue-400 dx-callsign"
on:click={() => handleSpotClick(spot)}
title="Click to send to Log4OM and tune radio">
{spot.DX}
</button>
</td>
<td class="p-2 font-mono text-xs">{spot.FrequencyMhz}</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">{spot.Band}</span>
</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded text-xs">{spot.Mode}</span>
</td>
<td class="p-2 text-slate-300 text-xs">{spot.SpotterCallsign}</td>
<td class="p-2 text-slate-400 text-xs">{spot.UTCTime}</td>
<td class="p-2 text-slate-400 text-xs">{spot.CountryName || 'N/A'}</td>
<td class="p-2">
{#if getStatusLabel(spot)}
<span class="px-1.5 py-0.5 rounded text-xs font-semibold border {getPriorityColor(spot)}">
{getStatusLabel(spot)}
</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
<!-- Header fixe -->
<div class="bg-slate-900/50 flex-shrink-0">
<div class="flex text-left text-xs text-slate-400 font-semibold">
<div class="p-2" style="width: 12%;">DX</div>
<div class="p-2" style="width: 12%;">Freq</div>
<div class="p-2" style="width: 8%;">Band</div>
<div class="p-2" style="width: 8%;">Mode</div>
<div class="p-2" style="width: 12%;">Spotter</div>
<div class="p-2" style="width: 8%;">Time</div>
<div class="p-2" style="width: 25%;">Country</div>
<div class="p-2" style="width: 15%;">Status</div>
</div>
</div>
<!-- Liste virtualisée -->
<div class="flex-1 overflow-hidden" bind:this={container}>
<VirtualList items={spots} {itemHeight} let:item>
<div class="flex border-b border-slate-700/30 hover:bg-slate-700/30 transition-colors text-sm" style="height: {itemHeight}px;">
<div class="p-2 flex items-center" style="width: 12%;">
<button
class="font-bold text-blue-400 hover:text-blue-300 transition-colors truncate w-full text-left"
on:click={() => handleSpotClick(item)}
title="Click to send to Log4OM and tune radio">
{item.DX}
</button>
</div>
<div class="p-2 flex items-center font-mono text-xs" style="width: 12%;">{item.FrequencyMhz}</div>
<div class="p-2 flex items-center" style="width: 8%;">
<span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">{item.Band}</span>
</div>
<div class="p-2 flex items-center" style="width: 8%;">
<span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded text-xs">{item.Mode}</span>
</div>
<div class="p-2 flex items-center text-slate-300 text-xs truncate" style="width: 12%;" title={item.SpotterCallsign}>
{item.SpotterCallsign}
</div>
<div class="p-2 flex items-center text-slate-400 text-xs" style="width: 8%;">{item.UTCTime}</div>
<div class="p-2 flex items-center text-slate-400 text-xs truncate" style="width: 25%;" title={item.CountryName || 'N/A'}>
{item.CountryName || 'N/A'}
</div>
<div class="p-2 flex items-center" style="width: 15%;">
{#if getStatusLabel(item)}
<span class="px-1.5 py-0.5 rounded text-xs font-semibold border {getPriorityColor(item)} truncate">
{getStatusLabel(item)}
</span>
{/if}
</div>
</div>
</VirtualList>
</div>
</div>

View File

@@ -8,7 +8,8 @@
warning: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>`,
info: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>`,
milestone: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path>`,
band: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>`
band: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>`,
mycall: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"></path>`
};
const colors = {
@@ -17,7 +18,8 @@
warning: 'bg-orange-500',
info: 'bg-blue-500',
milestone: 'bg-gradient-to-r from-purple-500 to-pink-500',
band: 'bg-gradient-to-r from-orange-500 to-amber-500'
band: 'bg-gradient-to-r from-orange-500 to-amber-500',
mycall: 'bg-gradient-to-r from-red-500 to-pink-500'
};
</script>

View File

@@ -0,0 +1,229 @@
// spotCache.js - Gestionnaire de cache IndexedDB pour les spots
class SpotCache {
constructor() {
this.dbName = 'FlexDXClusterDB';
this.version = 1;
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => {
console.error('IndexedDB failed to open');
reject(request.error);
};
request.onsuccess = () => {
this.db = request.result;
console.log('✅ IndexedDB initialized');
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Store pour les spots
if (!db.objectStoreNames.contains('spots')) {
const spotStore = db.createObjectStore('spots', { keyPath: 'ID' });
spotStore.createIndex('timestamp', 'timestamp', { unique: false });
spotStore.createIndex('band', 'Band', { unique: false });
console.log('Created spots store');
}
// Store pour les métadonnées (stats, watchlist, etc.)
if (!db.objectStoreNames.contains('metadata')) {
db.createObjectStore('metadata', { keyPath: 'key' });
console.log('Created metadata store');
}
// Store pour les QSOs récents
if (!db.objectStoreNames.contains('qsos')) {
const qsoStore = db.createObjectStore('qsos', { keyPath: 'id', autoIncrement: true });
qsoStore.createIndex('callsign', 'callsign', { unique: false });
console.log('Created qsos store');
}
};
});
}
// Sauvegarder les spots
async saveSpots(spots) {
if (!this.db || !spots || spots.length === 0) return;
try {
const transaction = this.db.transaction(['spots'], 'readwrite');
const store = transaction.objectStore('spots');
// Vider d'abord le store
await store.clear();
// Ajouter tous les spots avec un timestamp
const timestamp = Date.now();
spots.forEach(spot => {
store.put({ ...spot, timestamp });
});
await this.waitForTransaction(transaction);
console.log(`✅ Saved ${spots.length} spots to cache`);
} catch (error) {
console.error('Error saving spots:', error);
}
}
// Récupérer les spots du cache
async getSpots(maxAge = 5 * 60 * 1000) { // 5 minutes par défaut
if (!this.db) return [];
try {
const transaction = this.db.transaction(['spots'], 'readonly');
const store = transaction.objectStore('spots');
const request = store.getAll();
const spots = await this.waitForRequest(request);
// Vérifier l'âge du cache
if (spots.length > 0) {
const cacheAge = Date.now() - spots[0].timestamp;
if (cacheAge > maxAge) {
console.log('Cache too old, clearing...');
await this.clearSpots();
return [];
}
}
console.log(`📦 Loaded ${spots.length} spots from cache`);
return spots;
} catch (error) {
console.error('Error getting spots:', error);
return [];
}
}
// Supprimer les spots du cache
async clearSpots() {
if (!this.db) return;
try {
const transaction = this.db.transaction(['spots'], 'readwrite');
const store = transaction.objectStore('spots');
await store.clear();
await this.waitForTransaction(transaction);
console.log('🗑️ Cleared spots cache');
} catch (error) {
console.error('Error clearing spots:', error);
}
}
// Sauvegarder les métadonnées (stats, watchlist, etc.)
async saveMetadata(key, value) {
if (!this.db) return;
try {
const transaction = this.db.transaction(['metadata'], 'readwrite');
const store = transaction.objectStore('metadata');
await store.put({ key, value, timestamp: Date.now() });
await this.waitForTransaction(transaction);
} catch (error) {
console.error('Error saving metadata:', error);
}
}
// Récupérer les métadonnées
async getMetadata(key) {
if (!this.db) return null;
try {
const transaction = this.db.transaction(['metadata'], 'readonly');
const store = transaction.objectStore('metadata');
const request = store.get(key);
const result = await this.waitForRequest(request);
return result ? result.value : null;
} catch (error) {
console.error('Error getting metadata:', error);
return null;
}
}
// Sauvegarder les QSOs récents
async saveQSOs(qsos) {
if (!this.db || !qsos || qsos.length === 0) return;
try {
const transaction = this.db.transaction(['qsos'], 'readwrite');
const store = transaction.objectStore('qsos');
// Vider d'abord
await store.clear();
// Ajouter les QSOs
qsos.forEach((qso, index) => {
store.put({ ...qso, id: index + 1 });
});
await this.waitForTransaction(transaction);
console.log(`✅ Saved ${qsos.length} QSOs to cache`);
} catch (error) {
console.error('Error saving QSOs:', error);
}
}
// Récupérer les QSOs du cache
async getQSOs() {
if (!this.db) return [];
try {
const transaction = this.db.transaction(['qsos'], 'readonly');
const store = transaction.objectStore('qsos');
const request = store.getAll();
const qsos = await this.waitForRequest(request);
console.log(`📦 Loaded ${qsos.length} QSOs from cache`);
return qsos;
} catch (error) {
console.error('Error getting QSOs:', error);
return [];
}
}
// Utilitaires
waitForRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
waitForTransaction(transaction) {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}
// Fermer la base de données
close() {
if (this.db) {
this.db.close();
this.db = null;
console.log('IndexedDB closed');
}
}
// Supprimer complètement la base
async deleteDatabase() {
this.close();
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(this.dbName);
request.onsuccess = () => {
console.log('🗑️ Database deleted');
resolve();
};
request.onerror = () => reject(request.error);
});
}
}
// Export singleton
export const spotCache = new SpotCache();

View File

@@ -0,0 +1,92 @@
// spotWorker.js - Wrapper pour gérer le Web Worker
class SpotWorkerManager {
constructor() {
this.worker = null;
this.callbacks = new Map();
this.messageId = 0;
}
init() {
if (this.worker) return;
try {
this.worker = new Worker('/spot-worker.js');
this.worker.onmessage = (e) => {
const { type, data, messageId } = e.data;
// Appeler le callback correspondant si présent
if (messageId && this.callbacks.has(messageId)) {
const callback = this.callbacks.get(messageId);
callback(data);
this.callbacks.delete(messageId);
}
};
this.worker.onerror = (error) => {
console.error('Worker error:', error);
};
console.log('✅ Spot Worker initialized');
} catch (error) {
console.error('Failed to initialize worker:', error);
}
}
filterSpots(spots, filters, watchlist) {
return new Promise((resolve) => {
if (!this.worker) {
console.warn('Worker not initialized, filtering on main thread');
resolve(spots);
return;
}
const messageId = ++this.messageId;
this.callbacks.set(messageId, (filteredSpots) => {
resolve(filteredSpots);
});
this.worker.postMessage({
type: 'FILTER_SPOTS',
messageId,
data: { spots, filters, watchlist }
});
});
}
sortSpots(spots, sortBy = 'id', sortOrder = 'desc') {
return new Promise((resolve) => {
if (!this.worker) {
console.warn('Worker not initialized, sorting on main thread');
resolve(spots);
return;
}
const messageId = ++this.messageId;
this.callbacks.set(messageId, (sortedSpots) => {
resolve(sortedSpots);
});
this.worker.postMessage({
type: 'SORT_SPOTS',
messageId,
data: { spots, sortBy, sortOrder }
});
});
}
terminate() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
this.callbacks.clear();
console.log('Worker terminated');
}
}
}
// Export singleton
export const spotWorker = new SpotWorkerManager();

View File

@@ -1 +1 @@
["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA","PJ6Y","5J0EA","5K0UA","VP2M","5X1XA","C5R","C5LT","EL2BG","4X6TT","V85NPV","YI1MB","C21TS","XV9","TJ1GD","PZ5RA"]
["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA","PJ6Y","5J0EA","5K0UA","VP2M","5X1XA","C5R","C5LT","EL2BG","4X6TT","V85NPV","YI1MB","C21TS","XV9","TJ1GD"]