This commit is contained in:
2025-10-15 20:38:12 +05:30
parent 7e1b790d13
commit 33ae9aebad
7 changed files with 134 additions and 178 deletions

View File

@@ -56,7 +56,7 @@ type Config struct {
Skimmer bool `yaml:"skimmer"`
FT8 bool `yaml:"ft8"`
FT4 bool `yaml:"ft4"`
Command string `yanl:"command"`
Command string `yaml:"command"`
LoginPrompt string `yaml:"login_prompt"`
} `yaml:"cluster"`
@@ -93,7 +93,7 @@ func NewConfig(configPath string) *Config {
d := yaml.NewDecoder(file)
if err := d.Decode(&Cfg); err != nil {
log.Println("could not decod config file")
log.Println("could not decode config file")
}
return Cfg

View File

@@ -7,11 +7,9 @@
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,
@@ -68,65 +66,20 @@
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
let isShuttingDown = false;
let filterTimeout;
$: {
if (spotFilters.showAll) {
filteredSpots = spots;
} else {
filteredSpots = applyFilters(spots, spotFilters, watchlist);
if (filterTimeout) clearTimeout(filterTimeout);
filterTimeout = setTimeout(() => {
filteredSpots = applyFilters(spots, spotFilters, watchlist);
}, 150);
}
}
$: if (typeof localStorage !== 'undefined') {
localStorage.setItem('soundEnabled', soundEnabled.toString());
}
// Détecter les nouveaux spots et jouer les sons appropriés
$: if (spots.length > 0 && soundEnabled) {
checkForNewSpots(spots, previousSpots, watchlist);
// ✅ Ne garder que les 100 derniers spots pour la comparaison
previousSpots = spots.slice(0, 100);
}
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 ||
@@ -242,59 +195,64 @@
}
}
function connectWebSocket() {
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
return;
}
wsStatus = 'connecting';
try {
ws = new WebSocket('ws://localhost:8080/api/ws');
function connectWebSocket() {
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
return;
}
ws.onopen = () => {
console.log('WebSocket connected');
wsStatus = 'connected';
reconnectAttempts = 0;
errorMessage = '';
showToast('Connected to server', 'success');
};
wsStatus = 'connecting';
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
wsStatus = 'disconnected';
};
ws.onclose = () => {
console.log('WebSocket closed');
wsStatus = 'disconnected';
try {
ws = new WebSocket('ws://localhost:8080/api/ws');
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
errorMessage = `Connection lost. Reconnecting in ${delay/1000}s... (attempt ${reconnectAttempts}/${maxReconnectAttempts})`;
ws.onopen = () => {
console.log('WebSocket connected');
wsStatus = 'connected';
reconnectAttempts = 0;
errorMessage = '';
showToast('Connected to server', 'success');
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
wsStatus = 'disconnected';
};
ws.onclose = () => {
console.log('WebSocket closed');
wsStatus = 'disconnected';
reconnectTimer = setTimeout(() => {
connectWebSocket();
}, delay);
} else {
errorMessage = 'Unable to connect to server. Please refresh the page.';
}
};
} catch (error) {
console.error('Error creating WebSocket:', error);
wsStatus = 'disconnected';
if (isShuttingDown) {
console.log('App is shutting down, skip reconnection');
return;
}
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
errorMessage = `Connection lost. Reconnecting in ${delay/1000}s... (attempt ${reconnectAttempts}/${maxReconnectAttempts})`;
reconnectTimer = setTimeout(() => {
connectWebSocket();
}, delay);
} else {
errorMessage = 'Unable to connect to server. Please refresh the page.';
}
};
} catch (error) { // ✅ AJOUTER cette ligne
console.error('Error creating WebSocket:', error);
wsStatus = 'disconnected';
} // ✅ AJOUTER cette ligne
}
}
function handleWebSocketMessage(message) {
switch (message.type) {
@@ -324,9 +282,6 @@
const milestoneData = message.data;
const toastType = milestoneData.type === 'qso' ? 'milestone' : 'band';
showToast(milestoneData.message, toastType);
if (soundEnabled) {
playSound('milestone');
}
break;
}
}
@@ -395,62 +350,64 @@
}
}
async function shutdownApp() {
try {
const response = await fetch('/api/shutdown', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (data.success) {
showToast('FlexDXCluster shutting down...', 'info');
if (ws) ws.close();
if (reconnectTimer) clearTimeout(reconnectTimer);
wsStatus = 'disconnected';
maxReconnectAttempts = 0;
setTimeout(() => {
document.body.innerHTML = `
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
<div class="text-center">
<svg class="w-24 h-24 mx-auto mb-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h1 class="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
FlexDXCluster Stopped
</h1>
<p class="text-slate-400 text-lg">The application has been shut down successfully.</p>
<p class="text-slate-500 text-sm mt-4">You can close this window.</p>
</div>
async function shutdownApp() {
try {
// ✅ Désactiver la reconnexion et masquer l'erreur IMMÉDIATEMENT
isShuttingDown = true;
errorMessage = '';
maxReconnectAttempts = 0;
// ✅ Fermer le WebSocket proprement
if (ws) {
ws.onclose = null; // Désactiver le handler de reconnexion
ws.close();
}
if (reconnectTimer) clearTimeout(reconnectTimer);
wsStatus = 'disconnected';
showToast('FlexDXCluster shutting down...', 'info');
// ✅ Envoyer la commande de shutdown au backend
const response = await fetch('/api/shutdown', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (data.success) {
// ✅ Afficher le message de shutdown après un court délai
setTimeout(() => {
document.body.innerHTML = `
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
<div class="text-center">
<svg class="w-24 h-24 mx-auto mb-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h1 class="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
FlexDXCluster Stopped
</h1>
<p class="text-slate-400 text-lg">The application has been shut down successfully.</p>
<p class="text-slate-500 text-sm mt-4">You can close this window.</p>
</div>
`;
}, 1000);
}
} catch (error) {
console.error('Error shutting down:', error);
</div>
`;
}, 500);
}
} catch (error) {
console.error('Error shutting down:', error);
if (!isShuttingDown) {
showToast(`Cannot shutdown: ${error.message}`, 'error');
}
}
}
onMount(() => {
const savedSoundEnabled = localStorage.getItem('soundEnabled');
if (savedSoundEnabled !== null) {
soundEnabled = savedSoundEnabled === 'true';
}
connectWebSocket();
fetchSolarData();
const solarInterval = setInterval(fetchSolarData, 15 * 60 * 1000);
const cleanupInterval = setInterval(() => {
// Nettoyer previousSpots
if (previousSpots.length > 100) {
previousSpots = previousSpots.slice(0, 100);
}
}, 60000);
const handleSendSpot = (e) => {
sendCallsign(e.detail.callsign, e.detail.frequency, e.detail.mode);
};
@@ -466,8 +423,6 @@
</script>
<div class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white min-h-screen p-2">
<!-- Gestionnaire de sons -->
<SoundManager bind:enabled={soundEnabled} />
{#if errorMessage}
<ErrorBanner message={errorMessage} on:close={() => errorMessage = ''} />
@@ -481,9 +436,7 @@
{stats}
{solarData}
{wsStatus}
{soundEnabled}
on:shutdown={shutdownApp}
on:toggleSound={() => soundEnabled = !soundEnabled}
/>
<StatsCards

View File

@@ -1,7 +1,6 @@
<script>
import { createEventDispatcher } from 'svelte';
export let soundEnabled = true;
export let stats;
export let solarData;
export let wsStatus;
@@ -100,25 +99,6 @@
<span class="w-2 h-2 {stats.flexStatus === 'connected' ? 'bg-green-500 animate-pulse' : 'bg-red-500'} rounded-full"></span>
Flex
</span>
<!-- Bouton Son -->
<button
on:click={() => dispatch('toggleSound')}
class="px-3 py-1.5 rounded-lg transition-colors flex items-center gap-2 text-sm {soundEnabled ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-slate-700/50 hover:bg-slate-700 text-slate-300'}"
title={soundEnabled ? 'Mute sounds' : 'Enable sounds'}>
{#if soundEnabled}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path>
</svg>
<span>Sound On</span>
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" clip-rule="evenodd"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2"></path>
</svg>
<span>Sound Off</span>
{/if}
</button>
<button
on:click={() => dispatch('shutdown')}

View File

@@ -302,7 +302,7 @@ func (s *HTTPServer) broadcastUpdates() {
s.broadcast <- WSMessage{Type: "stats", Data: stats}
// Broadcast spots
spots := s.FlexRepo.GetAllSpots("300")
spots := s.FlexRepo.GetAllSpots("0")
s.checkBandOpening(spots)
s.broadcast <- WSMessage{Type: "spots", Data: spots}

18
package-lock.json generated Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "FlexDXClusterGui",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"svelte-virtual-list": "^3.0.1"
}
},
"node_modules/svelte-virtual-list": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svelte-virtual-list/-/svelte-virtual-list-3.0.1.tgz",
"integrity": "sha512-e7e+jn8VDjmdD8A1oOVBWmUi7jg+uugmiihkK0Cuk35JeeCR8ja40/B6DpCTHJFsQAZg6EVPjetN2ibgr25YCA==",
"license": "LIL"
}
}
}

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"svelte-virtual-list": "^3.0.1"
}
}

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"]
["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"]