diff --git a/TCPClient.go b/TCPClient.go index 9fe940b..5c1e76b 100644 --- a/TCPClient.go +++ b/TCPClient.go @@ -206,6 +206,11 @@ func (c *TCPClient) SetFilters() { Log.Info("FT4: On") } + if Cfg.Cluster.Beacon { + c.Write([]byte("set/beacon\r\n")) + Log.Info("Beacon: On") + } + if !Cfg.Cluster.FT8 { c.Write([]byte("set/noft8\r\n")) Log.Info("FT8: Off") @@ -220,6 +225,11 @@ func (c *TCPClient) SetFilters() { c.Write([]byte("set/noskimmer\r\n")) Log.Info("Skimmer: Off") } + + if !Cfg.Cluster.Beacon { + c.Write([]byte("set/nobeacon\r\n")) + Log.Info("Beacon: Off") + } } func (c *TCPClient) ReadLine() { diff --git a/config default.yml b/config default.yml index 906e3e8..3934bc5 100644 --- a/config default.yml +++ b/config default.yml @@ -1,10 +1,12 @@ general: delete_log_file_at_start: true callsign: XXXXX # Log4OM Callsign used to check if you get spotted by someone + QRALocator: JN36dg log_to_file: true log_level: INFO # INFO or DEBUG or WARN telnetserver: true # not in use for now flexradiospot: true # not in use for now + sendFreqModeToLog4OM: false # if not using a Flex then turn this on to send Freq and Mode to Log4OM which should in turn change the freq on the radio # Spot colors, if empty then default, colors in HEX AARRGGBB format spot_color_new_entity: background_color_new_entity: @@ -38,6 +40,7 @@ cluster: skimmer: true ft8: false ft4: false + beacon: false command: "SET/FILTER DOC/PASS 1A,3A,4O,9A,9H,C3,CT,CU,DL,E7,EA,EA6,EI,ER,ES,EU,F,G,GD,GI,GJ,GM,GU,GW,HA,HB,HB0,HV,I,IS,IT9,JW,JX,LA,LX,LY,LZ,OE,OH,OH0,OJ0,OK,OM,ON,OY,OZ,PA,S5,SM,SP,SV,SV5,SV9,T7,TA1,TF,TK,UA,UR,YL,YO,YU,Z6,Z3" #"SET/FILTER DOC/PASS 1A,3A,4O,9A,9H,C3,CT,CU,DL,E7,EA,EA6,EI,ER,ES,EU,F,G,GD,GI,GJ,GM,GU,GW,HA,HB,HB0,HV,I,IS,IT9,JW,JX,LA,LX,LY,LZ,OE,OH,OH0,OJ0,OK,OM,ON,OY,OZ,PA,S5,SM,SP,SV,SV5,SV9,T7,TA1,TF,TK,UA,UR,YL,YO,YU,Z6,Z3,ZA,ZB" login_prompt: "login:" flex: diff --git a/config.go b/config.go index 9591abd..6a60939 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,7 @@ type Config struct { TelnetServer bool `yaml:"telnetserver"` FlexRadioSpot bool `yaml:"flexradiospot"` SpotColorNewEntity string `yaml:"spot_color_new_entity"` + sendFreqModeToLog4OM bool `yaml:"sendFreqModeToLog4OM"` BackgroundColorNewEntity string `yaml:"background_color_new_entity"` SpotColorNewBand string `yaml:"spot_color_new_band"` BackgroundColorNewBand string `yaml:"background_color_new_band"` @@ -56,6 +57,7 @@ type Config struct { Skimmer bool `yaml:"skimmer"` FT8 bool `yaml:"ft8"` FT4 bool `yaml:"ft4"` + Beacon bool `yaml:"beacon"` Command string `yaml:"command"` LoginPrompt string `yaml:"login_prompt"` } `yaml:"cluster"` diff --git a/flexradio.go b/flexradio.go index 551860d..ed2b0c5 100644 --- a/flexradio.go +++ b/flexradio.go @@ -315,7 +315,7 @@ func (fc *FlexClient) ReadLine() { } // Sending the callsign to Log4OM - SendUDPMessage("" + spot.DX) + SendUDPMessage([]byte("" + spot.DX)) } // Status when a spot is deleted diff --git a/frontend/public/spot-worker.js b/frontend/public/spot-worker.js index 024b6a5..b318f91 100644 --- a/frontend/public/spot-worker.js +++ b/frontend/public/spot-worker.js @@ -1,22 +1,26 @@ // spot-worker.js - Web Worker pour traiter les spots +let lastProcessedData = null; + self.onmessage = function(e) { - - const { type, data, messageId } = e.data; - + const { type, data, messageId } = e.data; + + // Libérer l'ancienne référence + lastProcessedData = null; + switch(type) { case 'FILTER_SPOTS': const filtered = filterSpots(data.spots, data.filters, data.watchlist); - // ✅ AJOUTER messageId à la réponse self.postMessage({ type: 'FILTERED_SPOTS', data: filtered, messageId }); + lastProcessedData = null; break; - + case 'SORT_SPOTS': const sorted = sortSpots(data.spots, data.sortBy, data.sortOrder); - // ✅ AJOUTER messageId à la réponse self.postMessage({ type: 'SORTED_SPOTS', data: sorted, messageId }); + lastProcessedData = null; break; - + default: console.error('Unknown worker message type:', type); } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index c0d914d..d32af5d 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -78,13 +78,27 @@ $: { if (spotFilters.showAll) { filteredSpots = spots; + isFiltering = false; + if (filterTimeout) { + clearTimeout(filterTimeout); + filterTimeout = null; + } } else { - if (filterTimeout) clearTimeout(filterTimeout); + if (filterTimeout) { + clearTimeout(filterTimeout); + } filterTimeout = setTimeout(async () => { isFiltering = true; - filteredSpots = await spotWorker.filterSpots(spots, spotFilters, watchlist); - isFiltering = false; + try { + filteredSpots = await spotWorker.filterSpots(spots, spotFilters, watchlist); + } catch (error) { + console.error('Filter error:', error); + filteredSpots = spots; + } finally { + isFiltering = false; + filterTimeout = null; + } }, 150); } } @@ -220,7 +234,7 @@ wsStatus = 'connected'; reconnectAttempts = 0; errorMessage = ''; - showToast('Connected to server', 'success'); + showToast('✅ Connected to DX Cluster', 'connection'); }; ws.onmessage = (event) => { @@ -273,9 +287,9 @@ case 'spots': const newSpots = message.data || []; + // Détecter si votre indicatif a été spotté 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( @@ -285,16 +299,22 @@ } }); - if (notifiedSpots.size > 100) { + // ✅ Nettoyer les anciens IDs (garder seulement 200 derniers) + if (notifiedSpots.size > 200) { const arr = Array.from(notifiedSpots); - notifiedSpots = new Set(arr.slice(-100)); + notifiedSpots = new Set(arr.slice(-200)); } } spots = newSpots; + // ✅ Debounce la sauvegarde du cache (toutes les 30 secondes max) if (spots.length > 0) { - spotCache.saveSpots(spots).catch(err => console.error('Cache save error:', err)); + if (window.cacheSaveTimeout) clearTimeout(window.cacheSaveTimeout); + window.cacheSaveTimeout = setTimeout(() => { + spotCache.saveSpots(spots).catch(err => console.error('Cache save error:', err)); + window.cacheSaveTimeout = null; // ✅ Nettoyer la référence + }, 30000); // 30 secondes } break; case 'spotters': @@ -359,13 +379,13 @@ const data = await response.json(); if (data.success) { - showToast(`${callsign} Sent - Radio tuned on ${frequency} in ${mode}`, 'success'); + showToast(`📻 Tuned to ${callsign} • ${frequency} • ${mode}`, 'radio'); } else { - showToast('Failed to send', 'error'); + showToast('❌ Failed to send to radio', 'error'); } } catch (error) { console.error('Error sending callsign:', error); - showToast(`Error: ${error.message}`, 'error'); + showToast(`❌ Connection error: ${error.message}`, 'error'); } } @@ -380,11 +400,13 @@ const data = await response.json(); if (data.success) { stats.filters[filterName] = value; - showToast(`Filter ${filterName} updated`, 'success'); + const filterLabel = filterName.toUpperCase(); + const status = value ? 'ON' : 'OFF'; + showToast(`🔧 ${filterLabel} filter ${status}`, 'success'); } } catch (error) { console.error('Error updating filter:', error); - showToast(`Update error: ${error.message}`, 'error'); + showToast(`❌ Failed to update filter: ${error.message}`, 'error'); } } @@ -403,7 +425,7 @@ async function shutdownApp() { if (reconnectTimer) clearTimeout(reconnectTimer); wsStatus = 'disconnected'; - showToast('FlexDXCluster shutting down...', 'info'); + showToast('⚡ Shutting down FlexDXCluster...', 'warning'); // ✅ Envoyer la commande de shutdown au backend const response = await fetch('/api/shutdown', { @@ -434,7 +456,7 @@ async function shutdownApp() { } catch (error) { console.error('Error shutting down:', error); if (!isShuttingDown) { - showToast(`Cannot shutdown: ${error.message}`, 'error'); + showToast(`❌ Shutdown failed: ${error.message}`, 'error'); } } } @@ -496,6 +518,26 @@ async function shutdownApp() { window.removeEventListener('sendSpot', handleSendSpot); }; }); + + onDestroy(() => { + console.log('Cleaning up App...'); + + // ✅ Nettoyer tous les timeouts + if (filterTimeout) { + clearTimeout(filterTimeout); + filterTimeout = null; + } + + if (window.cacheSaveTimeout) { + clearTimeout(window.cacheSaveTimeout); + window.cacheSaveTimeout = null; + } + + notifiedSpots.clear(); + + console.log('App cleanup complete'); + }); +
diff --git a/frontend/src/components/StatsCards.svelte b/frontend/src/components/StatsCards.svelte index 3885e3b..76829e5 100644 --- a/frontend/src/components/StatsCards.svelte +++ b/frontend/src/components/StatsCards.svelte @@ -51,7 +51,7 @@

Clients

-
+
FT4 + + +
\ No newline at end of file diff --git a/frontend/src/components/Toast.svelte b/frontend/src/components/Toast.svelte index c0ab598..54d7ac8 100644 --- a/frontend/src/components/Toast.svelte +++ b/frontend/src/components/Toast.svelte @@ -1,6 +1,6 @@ -
-
- {#if icons[type]} - - {@html icons[type]} - - {/if} - {message} +
+
+
+ {#if icons[type]} +
+ + {@html icons[type]} + +
+ {/if} +
+

{message}

+
+
@@ -47,6 +63,6 @@ } .animate-in { - animation: slide-in-from-bottom 0.3s ease-out; + animation: slide-in-from-bottom 0.3s cubic-bezier(0.16, 1, 0.3, 1); } \ No newline at end of file diff --git a/frontend/src/lib/spotCache.js b/frontend/src/lib/spotCache.js index 1142430..ae9a68d 100644 --- a/frontend/src/lib/spotCache.js +++ b/frontend/src/lib/spotCache.js @@ -57,14 +57,24 @@ class SpotCache { const transaction = this.db.transaction(['spots'], 'readwrite'); const store = transaction.objectStore('spots'); - // Vider d'abord le store + // ✅ Vider d'abord le store await store.clear(); - // Ajouter tous les spots avec un timestamp + // ✅ Sauvegarder par batch de 100 pour éviter surcharge mémoire const timestamp = Date.now(); - spots.forEach(spot => { - store.put({ ...spot, timestamp }); - }); + const batchSize = 100; + + for (let i = 0; i < spots.length; i += batchSize) { + const batch = spots.slice(i, i + batchSize); + batch.forEach(spot => { + store.put({ ...spot, timestamp }); + }); + + // ✅ Petite pause pour libérer la mémoire + if (i + batchSize < spots.length) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + } await this.waitForTransaction(transaction); console.log(`✅ Saved ${spots.length} spots to cache`); diff --git a/frontend/src/lib/spotWorker.js b/frontend/src/lib/spotWorker.js index 6d6748a..accb23f 100644 --- a/frontend/src/lib/spotWorker.js +++ b/frontend/src/lib/spotWorker.js @@ -44,7 +44,17 @@ class SpotWorkerManager { const messageId = ++this.messageId; + // ✅ Créer un timeout pour éviter les callbacks orphelins + const timeoutId = setTimeout(() => { + if (this.callbacks.has(messageId)) { + console.warn('Worker callback timeout, cleaning up'); + this.callbacks.delete(messageId); + resolve(spots); // Fallback sur les spots non filtrés + } + }, 5000); // 5 secondes max + this.callbacks.set(messageId, (filteredSpots) => { + clearTimeout(timeoutId); // ✅ Nettoyer le timeout resolve(filteredSpots); }); @@ -66,7 +76,17 @@ class SpotWorkerManager { const messageId = ++this.messageId; + // ✅ Timeout pour éviter les callbacks orphelins + const timeoutId = setTimeout(() => { + if (this.callbacks.has(messageId)) { + console.warn('Worker callback timeout, cleaning up'); + this.callbacks.delete(messageId); + resolve(spots); + } + }, 5000); + this.callbacks.set(messageId, (sortedSpots) => { + clearTimeout(timeoutId); // ✅ Nettoyer le timeout resolve(sortedSpots); }); diff --git a/go.mod b/go.mod index 7e9be39..9998e7b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 4232a72..4bf286f 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= diff --git a/httpserver.go b/httpserver.go index b9e741e..d825914 100644 --- a/httpserver.go +++ b/httpserver.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/google/uuid" "github.com/gorilla/mux" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" @@ -58,6 +59,7 @@ type Filters struct { Skimmer bool `json:"skimmer"` FT8 bool `json:"ft8"` FT4 bool `json:"ft4"` + Beacon bool `json:"beacon"` } type APIResponse struct { @@ -91,6 +93,20 @@ type WatchlistSpot struct { WorkedBandMode bool `json:"workedBandMode"` } +type RemoteControlRequestFreq struct { + XMLName xml.Name `xml:"RemoteControlRequest"` + MessageId string `xml:"MessageId"` + RemoteControlMessage string `xml:"RemoteControlMessage"` + Frequency string `xml:"Frequency"` +} + +type RemoteControlRequestMode struct { + XMLName xml.Name `xml:"RemoteControlRequest"` + MessageId string `xml:"MessageId"` + RemoteControlMessage string `xml:"RemoteControlMessage"` + Mode string `xml:"Mode"` +} + var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true // Allow all origins in development @@ -461,6 +477,7 @@ func (s *HTTPServer) calculateStats() Stats { Skimmer: Cfg.Cluster.Skimmer, FT8: Cfg.Cluster.FT8, FT4: Cfg.Cluster.FT4, + Beacon: Cfg.Cluster.Beacon, }, } } @@ -523,6 +540,7 @@ type FilterRequest struct { Skimmer *bool `json:"skimmer,omitempty"` FT8 *bool `json:"ft8,omitempty"` FT4 *bool `json:"ft4,omitempty"` + Beacon *bool `json:"beacon,omitempty"` } func (s *HTTPServer) updateFilters(w http.ResponseWriter, r *http.Request) { @@ -564,6 +582,16 @@ func (s *HTTPServer) updateFilters(w http.ResponseWriter, r *http.Request) { } } + if req.Beacon != nil { + if *req.Beacon { + commands = append(commands, "set/beacon") + Cfg.Cluster.Beacon = true + } else { + commands = append(commands, "set/nobeacon") + Cfg.Cluster.Beacon = false + } + } + for _, cmd := range commands { s.TCPClient.CmdChan <- cmd } @@ -760,9 +788,39 @@ func (s *HTTPServer) handleSendCallsign(w http.ResponseWriter, r *http.Request) return } - SendUDPMessage("" + req.Callsign) + SendUDPMessage([]byte("" + req.Callsign)) s.Log.Infof("Sent callsign %s to Log4OM via UDP (127.0.0.1:2241)", req.Callsign) + if Cfg.General.sendFreqModeToLog4OM { + freqLog4OM := strings.Replace(req.Frequency, ".", "", 1) + + xmlRequestFreq := RemoteControlRequestFreq{ + MessageId: uuid.New().String(), // Generate a new unique ID + RemoteControlMessage: "SetTxFrequency", // Note: Typo matches your required format + Frequency: freqLog4OM, + } + + xmlRequestMode := RemoteControlRequestMode{ + MessageId: uuid.New().String(), // Generate a new unique ID + RemoteControlMessage: "SetMode", // Note: Typo matches your required format + Mode: req.Mode, + } + + xmlBytesFreq, err := xml.MarshalIndent(xmlRequestFreq, "", " ") + if err != nil { + s.Log.Errorf("Failed to marshal XML: %v", err) + } else { + SendUDPMessage([]byte(xmlBytesFreq)) + } + + xmlBytesMode, err := xml.MarshalIndent(xmlRequestMode, "", " ") + if err != nil { + s.Log.Errorf("Failed to marshal XML: %v", err) + } else { + SendUDPMessage([]byte(xmlBytesMode)) + } + } + if req.Frequency != "" && s.FlexClient != nil && s.FlexClient.IsConnected { tuneCmd := fmt.Sprintf("C%v|slice tune 0 %s", CommandNumber, req.Frequency) s.FlexClient.Write(tuneCmd) diff --git a/utils.go b/utils.go index f0268ec..915748d 100644 --- a/utils.go +++ b/utils.go @@ -59,12 +59,12 @@ func CheckSignal(TCPClient *TCPClient, TCPServer *TCPServer, FlexClient *FlexCli } } -func SendUDPMessage(message string) { +func SendUDPMessage(data []byte) { conn, err := net.Dial("udp", "127.0.0.1:2241") if err != nil { fmt.Printf("Some error %v", err) return } - conn.Write([]byte(message)) + conn.Write(data) conn.Close() }