diff --git a/Makefile b/Makefile
index ec279e7..6e85b38 100644
--- a/Makefile
+++ b/Makefile
@@ -25,13 +25,13 @@ help:
## install-deps: Installe les dépendances npm
install-deps:
- @echo "[1/2] Installation des dépendances npm..."
+ @echo "[1/2] Installation des dependances npm..."
cd $(FRONTEND_DIR) && npm install
- @echo "Dépendances installées"
+ @echo "Dependances installees"
@echo ""
- @echo "[2/2] Vérification de Go..."
+ @echo "[2/2] Verification de Go..."
@go version
- @echo "Go est installé"
+ @echo "Go est installe"
## frontend: Build le frontend Svelte
frontend:
diff --git a/flex.sqlite-journal b/flex.sqlite-journal
new file mode 100644
index 0000000..a5f868f
Binary files /dev/null and b/flex.sqlite-journal differ
diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index b603276..2d50616 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -7,9 +7,11 @@
import Sidebar from './components/Sidebar.svelte';
import Toast from './components/Toast.svelte';
import ErrorBanner from './components/ErrorBanner.svelte';
+ import SoundManager from './components/SoundManager.svelte';
// State
let spots = [];
+ let previousSpots = []; // Pour détecter les nouveaux spots
let filteredSpots = [];
let stats = {
totalSpots: 0,
@@ -23,13 +25,14 @@
filters: { skimmer: false, ft8: false, ft4: false }
};
let topSpotters = [];
- let watchlist = [];
+ let watchlist = []; // ✅ Initialisé vide, sera rempli par WebSocket
let recentQSOs = [];
let logStats = { today: 0, thisWeek: 0, thisMonth: 0, total: 0 };
let dxccProgress = { worked: 0, total: 340, percentage: 0 };
let solarData = { sfi: 'N/A', sunspots: 'N/A', aIndex: 'N/A', kIndex: 'N/A' };
let activeTab = 'stats';
+ let showOnlyActive = false; // ✅ État global pour persister entre les onglets
let wsStatus = 'disconnected';
let errorMessage = '';
let toastMessage = '';
@@ -58,13 +61,15 @@
band12M: false,
band10M: false,
band6M: false
- };
+ };
// WebSocket
let ws;
let reconnectTimer;
let reconnectAttempts = 0;
let maxReconnectAttempts = 10;
+ let soundEnabled = true;
+ let isShuttingDown = false; // ✅ Flag pour éviter les erreurs pendant le shutdown
// Reactive filtered spots
$: {
@@ -75,6 +80,52 @@
}
}
+ // Détecter les nouveaux spots et jouer les sons appropriés
+ $: if (spots.length > 0 && soundEnabled) {
+ checkForNewSpots(spots, previousSpots, watchlist);
+ previousSpots = [...spots];
+ }
+
+ // ✅ SUPPRIMÉ - La watchlist est gérée côté serveur via WebSocket
+ // Les fonctions addToWatchlist et removeFromWatchlist ne sont plus nécessaires
+
+ function checkForNewSpots(currentSpots, prevSpots, wl) {
+ // Ne pas jouer de sons au chargement initial
+ if (prevSpots.length === 0) return;
+
+ // Créer un Set des IDs précédents pour une recherche rapide
+ const previousIds = new Set(prevSpots.map(s => `${s.DX}-${s.Frequency}-${s.Time}`));
+
+ // Trouver les nouveaux spots
+ const newSpots = currentSpots.filter(spot => {
+ const spotId = `${spot.DX}-${spot.Frequency}-${spot.Time}`;
+ return !previousIds.has(spotId);
+ });
+
+ if (newSpots.length === 0) return;
+
+ // Vérifier s'il y a un nouveau DXCC (priorité maximale)
+ const hasNewDXCC = newSpots.some(spot => spot.NewDXCC === true);
+ if (hasNewDXCC) {
+ playSound('newDXCC');
+ return; // Ne jouer qu'un seul son
+ }
+
+ // Vérifier s'il y a un spot de la watchlist
+ const hasWatchlistSpot = newSpots.some(spot =>
+ wl.some(pattern => spot.DX === pattern || spot.DX.startsWith(pattern))
+ );
+ if (hasWatchlistSpot) {
+ playSound('watchlist');
+ }
+ }
+
+ function playSound(type) {
+ window.dispatchEvent(new CustomEvent('playSound', {
+ detail: { type }
+ }));
+ }
+
function applyFilters(allSpots, filters, wl) {
const bandFiltersActive = filters.band160M || filters.band80M || filters.band60M ||
filters.band40M || filters.band30M || filters.band20M || filters.band17M ||
@@ -255,6 +306,7 @@
topSpotters = message.data || [];
break;
case 'watchlist':
+ // ✅ La watchlist est mise à jour par WebSocket
watchlist = message.data || [];
break;
case 'log':
@@ -344,13 +396,11 @@
if (data.success) {
showToast('FlexDXCluster shutting down...', 'info');
- // Fermer le WebSocket et arrêter les tentatives de reconnexion
if (ws) ws.close();
if (reconnectTimer) clearTimeout(reconnectTimer);
wsStatus = 'disconnected';
- maxReconnectAttempts = 0; // Empêcher les reconnexions
+ maxReconnectAttempts = 0;
- // Afficher la page de shutdown après 1 seconde
setTimeout(() => {
document.body.innerHTML = `
@@ -378,10 +428,8 @@
connectWebSocket();
fetchSolarData();
- // Update solar data every 15 minutes
const solarInterval = setInterval(fetchSolarData, 15 * 60 * 1000);
- // Listen for sendSpot events from watchlist
const handleSendSpot = (e) => {
sendCallsign(e.detail.callsign, e.detail.frequency, e.detail.mode);
};
@@ -397,6 +445,9 @@
+
+
+
{#if errorMessage}
errorMessage = ''} />
{/if}
@@ -424,8 +475,8 @@
on:toggleFilter={(e) => toggleFilter(e.detail)}
/>
-
-
+
+
-
showToast(e.detail.message, e.detail.type)}
- />
+
+ showToast(e.detail.message, e.detail.type)}
+ />
+
\ No newline at end of file
diff --git a/frontend/src/components/Sidebar.svelte b/frontend/src/components/Sidebar.svelte
index 0408f1a..49eeb86 100644
--- a/frontend/src/components/Sidebar.svelte
+++ b/frontend/src/components/Sidebar.svelte
@@ -11,11 +11,17 @@
export let recentQSOs;
export let logStats;
export let dxccProgress;
+ export let showOnlyActive = false; // ✅ Export pour persister l'état
const dispatch = createEventDispatcher();
+
+ // ✅ Propagation des évènements vers le parent
+ function handleToast(event) {
+ dispatch('toast', event.detail);
+ }
-
+
-
+
{#if activeTab === 'stats'}
- {:else if activeTab === 'watchlist'}
-
dispatch('toast', e.detail)}
+ {:else if activeTab === 'watchlist'}
+
{:else if activeTab === 'log'}
+ import { onMount } from 'svelte';
+
+ export let enabled = true;
+ export let volume = 0.3;
+ export let showControls = false; // Masquer les contrôles par défaut
+
+ let audioContext;
+ let isMuted = false;
+
+ onMount(() => {
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ window.addEventListener('playSound', handlePlaySound);
+
+ // Charger les préférences depuis localStorage
+ const savedMuted = localStorage.getItem('soundMuted');
+ const savedVolume = localStorage.getItem('soundVolume');
+
+ if (savedMuted !== null) isMuted = savedMuted === 'true';
+ if (savedVolume !== null) volume = parseFloat(savedVolume);
+
+ return () => {
+ window.removeEventListener('playSound', handlePlaySound);
+ if (audioContext) audioContext.close();
+ };
+ });
+
+ function handlePlaySound(event) {
+ if (!enabled || isMuted) return;
+
+ const { type } = event.detail;
+
+ if (type === 'newDXCC') {
+ playNewDXCCSound();
+ } else if (type === 'watchlist') {
+ playWatchlistSound();
+ }
+ }
+
+ function playNewDXCCSound() {
+ if (!audioContext) return;
+
+ const now = audioContext.currentTime;
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.type = 'sine';
+
+ // Mélodie joyeuse : C5 -> E5 -> G5 -> C6
+ oscillator.frequency.setValueAtTime(523.25, now);
+ oscillator.frequency.setValueAtTime(659.25, now + 0.15);
+ oscillator.frequency.setValueAtTime(783.99, now + 0.3);
+ oscillator.frequency.setValueAtTime(1046.50, now + 0.45);
+
+ gainNode.gain.setValueAtTime(0, now);
+ gainNode.gain.linearRampToValueAtTime(volume, now + 0.05);
+ gainNode.gain.setValueAtTime(volume * 0.8, now + 0.5);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.7);
+
+ oscillator.start(now);
+ oscillator.stop(now + 0.7);
+ }
+
+ function playWatchlistSound() {
+ if (!audioContext) return;
+
+ const now = audioContext.currentTime;
+ playBeep(now, 800, 0.1);
+ playBeep(now + 0.15, 1000, 0.1);
+ }
+
+ function playBeep(startTime, frequency, duration) {
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.type = 'sine';
+ oscillator.frequency.setValueAtTime(frequency, startTime);
+
+ gainNode.gain.setValueAtTime(0, startTime);
+ gainNode.gain.linearRampToValueAtTime(volume, startTime + 0.01);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + duration);
+
+ oscillator.start(startTime);
+ oscillator.stop(startTime + duration);
+ }
+
+ function toggleMute() {
+ isMuted = !isMuted;
+ localStorage.setItem('soundMuted', isMuted.toString());
+ }
+
+ function updateVolume(newVolume) {
+ volume = newVolume;
+ localStorage.setItem('soundVolume', volume.toString());
+ }
+
+
+{#if showControls}
+
+
+ {#if isMuted}
+
+
+
+
+ {:else}
+
+
+
+ {/if}
+
+
+
+
+
+
+
updateVolume(e.target.value / 100)}
+ class="w-20 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
+ title="Volume: {Math.round(volume * 100)}%"
+ />
+
{Math.round(volume * 100)}%
+
+
+{/if}
+
+
\ No newline at end of file
diff --git a/frontend/src/components/WatchlistTab.svelte b/frontend/src/components/WatchlistTab.svelte
index 601b417..c3b1d2a 100644
--- a/frontend/src/components/WatchlistTab.svelte
+++ b/frontend/src/components/WatchlistTab.svelte
@@ -1,25 +1,61 @@
-
+
Watchlist
showOnlyActive = !showOnlyActive}
+ on:click={toggleActiveOnly}
class="px-3 py-1.5 text-xs rounded transition-colors flex items-center gap-2 {showOnlyActive ? 'bg-blue-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
@@ -143,7 +184,7 @@
-
+
{#if displayList.length === 0}
@@ -186,7 +227,7 @@
{#if count > 0}
-
+
{#each matchingSpots.slice(0, 10) as spot}
sendSpot(spot)}
diff --git a/watchlist.json b/watchlist.json
index 335ddb4..a7d5566 100644
--- a/watchlist.json
+++ b/watchlist.json
@@ -1 +1 @@
-["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA","PJ6Y","5J0EA","5K0UA","VP2M","5X1XA","C5R","C5LT","EL2BG"]
\ No newline at end of file
+["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA","PJ6Y","5J0EA","5K0UA","VP2M","5X1XA","C5R","C5LT","EL2BG","4X6TT","V85NPV","YI1MB"]
\ No newline at end of file