From 0159c68fa529dc1dca6b944304769c7c22b80994 Mon Sep 17 00:00:00 2001 From: rouggy Date: Sun, 19 Oct 2025 10:15:11 +0200 Subject: [PATCH] bug --- TCPClient.go | 3 +- TCPServer.go | 68 +-- database.go | 19 +- frontend/src/App.svelte | 18 + frontend/src/components/LogsTab.svelte | 139 +++++ frontend/src/components/Sidebar.svelte | 16 +- frontend/src/components/SpotsTable.svelte | 5 +- frontend/src/components/StatsCards.svelte | 40 +- httpserver.go | 52 +- log.go | 130 ++++- loghook.go | 80 +++ main.go | 49 +- spot.go | 627 ++++++++++++---------- spotprocessor.go | 1 + stats.go | 45 ++ utils.go | 29 +- watchlist.json | 280 +++++----- 17 files changed, 1078 insertions(+), 523 deletions(-) create mode 100644 frontend/src/components/LogsTab.svelte create mode 100644 loghook.go create mode 100644 stats.go diff --git a/TCPClient.go b/TCPClient.go index ec27a8d..f8f161f 100644 --- a/TCPClient.go +++ b/TCPClient.go @@ -15,7 +15,7 @@ import ( log "github.com/sirupsen/logrus" ) -var spotRe *regexp.Regexp = regexp.MustCompile(`DX\sde\s([\w\d]+).*:\s+(\d+.\d)\s+([\w\d\/]+)\s+(CW|cw|SSB|ssb|FT8|ft8|FT4|ft4|RTTY|rtty|USB|usb|LSB|lsb)?\s+(.*)\s\s\s+([\d]+\w{1})`) +var spotRe *regexp.Regexp = regexp.MustCompile(`(?i)DX\sde\s([\w\d\-#]+).*?:\s*(\d+\.\d+)\s+([\w\d\/]+)\s+(?:(CW|SSB|FT8|FT4|RTTY|USB|LSB|FM)\s+)?(.+?)\s+(\d{4}Z)`) var defaultLoginRe *regexp.Regexp = regexp.MustCompile("[\\w\\d-_]+ login:") var defaultPasswordRe *regexp.Regexp = regexp.MustCompile("Password:") @@ -315,6 +315,7 @@ func (c *TCPClient) ReadLine() { } if strings.Contains(messageString, "DX") { + IncrementSpotsReceived() ProcessTelnetSpot(spotRe, messageString, c.SpotChanToFlex, c.SpotChanToHTTPServer, c.Countries, c.ContactRepo) } diff --git a/TCPServer.go b/TCPServer.go index b6bad56..ee917b9 100644 --- a/TCPServer.go +++ b/TCPServer.go @@ -16,31 +16,34 @@ var ( ) type TCPServer struct { - Address string - Port string - Clients map[net.Conn]bool - Mutex *sync.Mutex - LogWriter *bufio.Writer - Reader *bufio.Reader - Writer *bufio.Writer - Conn net.Conn - Listener net.Listener - MsgChan chan string - CmdChan chan string - Log *log.Logger - Config *Config - MessageSent int + Address string + Port string + Clients map[net.Conn]*ClientInfo // ✅ Map avec structure ClientInfo + Mutex *sync.Mutex + LogWriter *bufio.Writer + Reader *bufio.Reader + Writer *bufio.Writer + Conn net.Conn + Listener net.Listener + MsgChan chan string + CmdChan chan string + Log *log.Logger + Config *Config +} + +// ✅ Structure pour stocker les infos client +type ClientInfo struct { + ConnectedAt time.Time } func NewTCPServer(address string, port string) *TCPServer { return &TCPServer{ - Address: address, - Port: port, - Clients: make(map[net.Conn]bool), - MsgChan: make(chan string, 100), - CmdChan: make(chan string), - Mutex: new(sync.Mutex), - MessageSent: 0, + Address: address, + Port: port, + Clients: make(map[net.Conn]*ClientInfo), + MsgChan: make(chan string, 100), + CmdChan: make(chan string), + Mutex: new(sync.Mutex), } } @@ -68,8 +71,11 @@ func (s *TCPServer) StartServer() { Log.Error("Could not accept connections to telnet server") continue } + s.Mutex.Lock() - s.Clients[s.Conn] = true + s.Clients[s.Conn] = &ClientInfo{ + ConnectedAt: time.Now(), // ✅ Enregistre l'heure de connexion + } s.Mutex.Unlock() go s.handleConnection() @@ -77,7 +83,6 @@ func (s *TCPServer) StartServer() { } func (s *TCPServer) handleConnection() { - // Store the connection locally to avoid race conditions conn := s.Conn conn.Write([]byte("Welcome to the FlexDXCluster telnet server! Type 'bye' to exit.\n")) @@ -101,14 +106,13 @@ func (s *TCPServer) handleConnection() { message = strings.TrimSpace(message) - // if message is bye then disconnect if message == "bye" { Log.Infof("Client %s sent bye command", conn.RemoteAddr().String()) return } - if strings.Contains(message, "DX") || strings.Contains(message, "SH/DX") || strings.Contains(message, "set") || strings.Contains(message, "SET") { - // send DX spot to the client + if strings.Contains(message, "DX") || strings.Contains(message, "SH/DX") || + strings.Contains(message, "set") || strings.Contains(message, "SET") { select { case s.CmdChan <- message: Log.Debugf("Command from client %s: %s", conn.RemoteAddr().String(), message) @@ -135,24 +139,24 @@ func (s *TCPServer) broadcastMessage(message string) { return } - if s.MessageSent == 0 { - time.Sleep(3 * time.Second) - s.MessageSent = 1 + // ✅ Si un client vient de se connecter (< 3 secondes), NE RIEN ENVOYER + for _, info := range s.Clients { + if time.Since(info.ConnectedAt) < 3*time.Second { + // ✅ Client trop récent, on DROP le spot + return + } } - // Collect failed clients var failedClients []net.Conn for client := range s.Clients { _, err := client.Write([]byte(message + "\r\n")) - s.MessageSent++ if err != nil { Log.Warnf("Error sending to client %s: %v", client.RemoteAddr(), err) failedClients = append(failedClients, client) } } - // Remove failed clients for _, client := range failedClients { delete(s.Clients, client) client.Close() diff --git a/database.go b/database.go index 8c5d5c6..198da7d 100644 --- a/database.go +++ b/database.go @@ -122,6 +122,7 @@ func NewFlexDXDatabase(filePath string) *FlexDXClusterRepository { "timestamp" INTEGER, "lifeTime" TEXT, "priority" TEXT, + "originalComment" TEXT, "comment" TEXT, "color" TEXT, "backgroundColor" TEXT, @@ -458,7 +459,7 @@ func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot { s := FlexSpot{} for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, - &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { + &s.OriginalComment, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { return nil // Arrête le traitement s'il y a une erreur sur une ligne } @@ -481,7 +482,7 @@ func (r *FlexDXClusterRepository) FindDXSameBand(spot FlexSpot) (*FlexSpot, erro s := FlexSpot{} for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, - &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { + &s.OriginalComment, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { r.Log.Error(err) return nil, err } @@ -490,8 +491,8 @@ func (r *FlexDXClusterRepository) FindDXSameBand(spot FlexSpot) (*FlexSpot, erro } func (r *FlexDXClusterRepository) CreateSpot(spot FlexSpot) { - query := "INSERT INTO `spots` (`commandNumber`, `flexSpotNumber`, `dx`, `freqMhz`, `freqHz`, `band`, `mode`, `spotter`, `flexMode`, `source`, `time`, `timestamp`, `lifeTime`, `priority`, `comment`, `color`, `backgroundColor`, `countryName`, `dxcc`, `newDXCC`, `newBand`, `newMode`, `newSlot`, `worked`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - insertResult, err := r.db.ExecContext(context.Background(), query, spot.CommandNumber, spot.CommandNumber, spot.DX, spot.FrequencyMhz, spot.FrequencyHz, spot.Band, spot.Mode, spot.SpotterCallsign, spot.FlexMode, spot.Source, spot.UTCTime, time.Now().Unix(), spot.LifeTime, spot.Priority, spot.Comment, spot.Color, spot.BackgroundColor, spot.CountryName, spot.DXCC, spot.NewDXCC, spot.NewBand, spot.NewMode, spot.NewSlot, spot.Worked) + query := "INSERT INTO `spots` (`commandNumber`, `flexSpotNumber`, `dx`, `freqMhz`, `freqHz`, `band`, `mode`, `spotter`, `flexMode`, `source`, `time`, `timestamp`, `lifeTime`, `priority`, `originalComment`, `comment`, `color`, `backgroundColor`, `countryName`, `dxcc`, `newDXCC`, `newBand`, `newMode`, `newSlot`, `worked`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + insertResult, err := r.db.ExecContext(context.Background(), query, spot.CommandNumber, spot.CommandNumber, spot.DX, spot.FrequencyMhz, spot.FrequencyHz, spot.Band, spot.Mode, spot.SpotterCallsign, spot.FlexMode, spot.Source, spot.UTCTime, time.Now().Unix(), spot.LifeTime, spot.Priority, spot.OriginalComment, spot.Comment, spot.Color, spot.BackgroundColor, spot.CountryName, spot.DXCC, spot.NewDXCC, spot.NewBand, spot.NewMode, spot.NewSlot, spot.Worked) if err != nil { Log.Errorf("cannot insert spot in database: %s", err) } @@ -504,8 +505,8 @@ func (r *FlexDXClusterRepository) CreateSpot(spot FlexSpot) { } func (r *FlexDXClusterRepository) UpdateSpotSameBand(spot FlexSpot) error { - _, err := r.db.Exec(`UPDATE spots SET commandNumber = ?, DX = ?, freqMhz = ?, freqHz = ?, band = ?, mode = ?, spotter = ?, flexMode = ?, source = ?, time = ?, timestamp = ?, lifeTime = ?, priority = ?, comment = ?, color = ?, backgroundColor = ?, countryName = ?, dxcc = ?, newDXCC = ?, newBand = ?, newMode = ?, newSlot = ?, worked = ? WHERE DX = ? AND band = ?`, - spot.CommandNumber, spot.DX, spot.FrequencyMhz, spot.FrequencyHz, spot.Band, spot.Mode, spot.SpotterCallsign, spot.FlexMode, spot.Source, spot.UTCTime, spot.TimeStamp, spot.LifeTime, spot.Priority, spot.Comment, spot.Color, spot.BackgroundColor, spot.CountryName, spot.DXCC, spot.NewDXCC, spot.NewBand, spot.NewMode, spot.NewSlot, spot.Worked, spot.DX, spot.Band) + _, err := r.db.Exec(`UPDATE spots SET commandNumber = ?, DX = ?, freqMhz = ?, freqHz = ?, band = ?, mode = ?, spotter = ?, flexMode = ?, source = ?, time = ?, timestamp = ?, lifeTime = ?, priority = ?, originalComment = ?, comment = ?, color = ?, backgroundColor = ?, countryName = ?, dxcc = ?, newDXCC = ?, newBand = ?, newMode = ?, newSlot = ?, worked = ? WHERE DX = ? AND band = ?`, + spot.CommandNumber, spot.DX, spot.FrequencyMhz, spot.FrequencyHz, spot.Band, spot.Mode, spot.SpotterCallsign, spot.FlexMode, spot.Source, spot.UTCTime, spot.TimeStamp, spot.LifeTime, spot.Priority, spot.OriginalComment, spot.Comment, spot.Color, spot.BackgroundColor, spot.CountryName, spot.DXCC, spot.NewDXCC, spot.NewBand, spot.NewMode, spot.NewSlot, spot.Worked, spot.DX, spot.Band) if err != nil { r.Log.Errorf("could not update database: %s", err) return err @@ -525,7 +526,7 @@ func (r *FlexDXClusterRepository) FindSpotByCommandNumber(commandNumber string) s := FlexSpot{} for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, - &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { + &s.OriginalComment, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { r.Log.Error(err) return nil, err } @@ -545,7 +546,7 @@ func (r *FlexDXClusterRepository) FindSpotByFlexSpotNumber(spotNumber string) (* s := FlexSpot{} for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, - &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { + &s.OriginalComment, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { r.Log.Error(err) return nil, err } @@ -565,7 +566,7 @@ func (r *FlexDXClusterRepository) UpdateFlexSpotNumberByID(flexSpotNumber string s := FlexSpot{} for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, - &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { + &s.OriginalComment, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { r.Log.Error(err) return nil, err } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 7b214f3..d54438b 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -10,6 +10,7 @@ import ErrorBanner from './components/ErrorBanner.svelte'; import { spotWorker } from './lib/spotWorker.js'; import { spotCache } from './lib/spotCache.js'; + import LogsTab from './components/LogsTab.svelte'; // State @@ -39,6 +40,7 @@ let errorMessage = ''; let toastMessage = ''; let toastType = 'info'; + let logs = []; let spotFilters = { showAll: true, @@ -328,6 +330,21 @@ spotCache.saveQSOs(recentQSOs).catch(err => console.error('Cache save error:', err)); } break; + case 'appLog': + // Un seul log applicatif + if (message.data) { + logs = [...logs, message.data]; + // Garder seulement les 500 derniers + if (logs.length > 500) { + logs = logs.slice(-500); + } + } + break; + case 'appLogs': + // Logs initiaux (au chargement) + logs = message.data || []; + break; + case 'logStats': logStats = message.data || {}; break; @@ -608,6 +625,7 @@ async function shutdownApp() { {recentQSOs} {logStats} {dxccProgress} + {logs} on:toast={(e) => showToast(e.detail.message, e.detail.type)} /> diff --git a/frontend/src/components/LogsTab.svelte b/frontend/src/components/LogsTab.svelte new file mode 100644 index 0000000..f33b0f6 --- /dev/null +++ b/frontend/src/components/LogsTab.svelte @@ -0,0 +1,139 @@ + + +
+ +
+
+

Application Logs

+
+ + +
+
+ + +
+ Show: + + + + + + + + + + {filteredLogs.length} / {logs.length} logs +
+
+ + +
+ + {#if filteredLogs.length === 0} +
+ + + +

+ {logs.length === 0 ? 'No logs yet' : 'No logs matching selected levels'} +

+
+ {:else} + {#each filteredLogs as log (log.timestamp + log.message)} +
+ {log.timestamp} + + {log.level.toUpperCase()} + + {log.message} +
+ {/each} + {/if} +
+
\ No newline at end of file diff --git a/frontend/src/components/Sidebar.svelte b/frontend/src/components/Sidebar.svelte index 49eeb86..81e177f 100644 --- a/frontend/src/components/Sidebar.svelte +++ b/frontend/src/components/Sidebar.svelte @@ -3,6 +3,7 @@ import StatsTab from './StatsTab.svelte'; import WatchlistTab from './WatchlistTab.svelte'; import LogTab from './LogTab.svelte'; + import LogsTab from './LogsTab.svelte'; export let activeTab; export let topSpotters; @@ -12,6 +13,7 @@ export let logStats; export let dxccProgress; export let showOnlyActive = false; // ✅ Export pour persister l'état + export let logs = []; const dispatch = createEventDispatcher(); @@ -49,8 +51,18 @@ - Log + Log4OM + + + @@ -70,6 +82,8 @@ {logStats} {dxccProgress} /> + {:else if activeTab === 'logs'} + {/if} \ No newline at end of file diff --git a/frontend/src/components/SpotsTable.svelte b/frontend/src/components/SpotsTable.svelte index 6e5fa34..4640742 100644 --- a/frontend/src/components/SpotsTable.svelte +++ b/frontend/src/components/SpotsTable.svelte @@ -51,7 +51,6 @@ } function getCleanComment(spot) { - // Retirer le commentaire original brut s'il existe if (!spot.OriginalComment) return ''; return spot.OriginalComment.trim(); } @@ -67,11 +66,11 @@
DX
Country
+
Time
Freq
Band
Mode
Spotter
-
Time
Comment
Status
@@ -92,6 +91,7 @@
{item.CountryName || 'N/A'}
+
{item.UTCTime}
{item.FrequencyMhz}
{item.Band} @@ -102,7 +102,6 @@
{item.SpotterCallsign}
-
{item.UTCTime}
{getCleanComment(item)}
diff --git a/frontend/src/components/StatsCards.svelte b/frontend/src/components/StatsCards.svelte index 99c4779..639ed94 100644 --- a/frontend/src/components/StatsCards.svelte +++ b/frontend/src/components/StatsCards.svelte @@ -10,25 +10,49 @@ } -
- +
+
- - + + -
{stats.totalSpots}
+
{stats.spotsReceived || 0}
-

Total Spots

+

Received

+
+ + +
+
+ + + +
{stats.spotsProcessed || 0}
+
+

Processed

+
+ + +
+
+ + + +
+ {stats.spotSuccessRate ? stats.spotSuccessRate.toFixed(1) : '0.0'}% +
+
+

Success Rate

- + -
{stats.newDXCC}
+
{stats.newDXCC}

New DXCC

diff --git a/httpserver.go b/httpserver.go index 5154672..a937f80 100644 --- a/httpserver.go +++ b/httpserver.go @@ -21,6 +21,7 @@ import ( //go:embed frontend/dist/* var frontendFiles embed.FS +var httpServerInstance *HTTPServer type HTTPServer struct { Router *mux.Router @@ -52,6 +53,10 @@ type Stats struct { MyCallsign string `json:"myCallsign"` Mode string `json:"mode"` Filters Filters `json:"filters"` + SpotsReceived int64 `json:"spotsReceived"` + SpotsProcessed int64 `json:"spotsProcessed"` + SpotsRejected int64 `json:"spotsRejected"` + SpotSuccessRate float64 `json:"spotSuccessRate"` } type Filters struct { @@ -131,6 +136,8 @@ func NewHTTPServer(flexRepo *FlexDXClusterRepository, contactRepo *Log4OMContact lastBandOpening: make(map[string]time.Time), } + httpServerInstance = server + server.setupRoutes() go server.handleBroadcasts() go server.broadcastUpdates() @@ -162,6 +169,8 @@ func (s *HTTPServer) setupRoutes() { api.HandleFunc("/watchlist/remove", s.removeFromWatchlist).Methods("DELETE", "OPTIONS") api.HandleFunc("/watchlist/update-notes", s.updateWatchlistNotes).Methods("POST", "OPTIONS") api.HandleFunc("/watchlist/update-sound", s.updateWatchlistSound).Methods("POST", "OPTIONS") + api.HandleFunc("/stats/spots", s.getSpotProcessingStats).Methods("GET", "OPTIONS") + api.HandleFunc("/logs", s.getLogs).Methods("GET", "OPTIONS") // WebSocket endpoint api.HandleFunc("/ws", s.handleWebSocket).Methods("GET") @@ -268,6 +277,11 @@ func (s *HTTPServer) sendInitialData(conn *websocket.Conn) { "percentage": float64(dxccCount) / 340.0 * 100.0, } conn.WriteJSON(WSMessage{Type: "dxccProgress", Data: dxccData}) + + if logBuffer != nil { + logs := logBuffer.GetAll() + conn.WriteJSON(WSMessage{Type: "appLogs", Data: logs}) + } } func (s *HTTPServer) handleBroadcasts() { @@ -355,6 +369,30 @@ func (s *HTTPServer) broadcastUpdates() { } } +func (s *HTTPServer) getLogs(w http.ResponseWriter, r *http.Request) { + if logBuffer == nil { + s.sendJSON(w, APIResponse{Success: true, Data: []LogEntry{}}) + return + } + + logs := logBuffer.GetAll() + s.sendJSON(w, APIResponse{Success: true, Data: logs}) +} + +func (s *HTTPServer) getSpotProcessingStats(w http.ResponseWriter, r *http.Request) { + received, processed, rejected := GetSpotStats() + successRate := GetSpotSuccessRate() + + stats := map[string]interface{}{ + "received": received, + "processed": processed, + "rejected": rejected, + "successRate": successRate, + } + + s.sendJSON(w, APIResponse{Success: true, Data: stats}) +} + func (s *HTTPServer) checkQSOMilestones(todayCount int) { s.statsMutex.Lock() defer s.statsMutex.Unlock() @@ -463,6 +501,10 @@ func (s *HTTPServer) calculateStats() Stats { flexStatus = "connected" } + // Récupérer les stats de traitement des spots + received, processed, rejected := GetSpotStats() + successRate := GetSpotSuccessRate() + return Stats{ TotalSpots: len(allSpots), NewDXCC: newDXCCCount, @@ -477,6 +519,10 @@ func (s *HTTPServer) calculateStats() Stats { FT4: Cfg.Cluster.FT4, Beacon: Cfg.Cluster.Beacon, }, + SpotsReceived: received, + SpotsProcessed: processed, + SpotsRejected: rejected, + SpotSuccessRate: successRate, } } @@ -901,6 +947,7 @@ func (s *HTTPServer) sendJSON(w http.ResponseWriter, data interface{}) { json.NewEncoder(w).Encode(data) } +// ✅ Fonction de shutdown propre func (s *HTTPServer) shutdownApp(w http.ResponseWriter, r *http.Request) { s.Log.Info("Shutdown request received from dashboard") @@ -908,7 +955,10 @@ func (s *HTTPServer) shutdownApp(w http.ResponseWriter, r *http.Request) { go func() { time.Sleep(500 * time.Millisecond) - s.Log.Info("Initiating shutdown...") + + // ✅ Utiliser le shutdown centralisé + GracefulShutdown(s.TCPClient, s.TCPServer, s.FlexClient, s.FlexRepo, s.ContactRepo) + os.Exit(0) }() } diff --git a/log.go b/log.go index 60e5b86..498c9b6 100644 --- a/log.go +++ b/log.go @@ -1,42 +1,102 @@ package main import ( + "context" + "fmt" "io" "os" + "path/filepath" + "sync" + "time" log "github.com/sirupsen/logrus" prefixed "github.com/x-cray/logrus-prefixed-formatter" ) var Log *log.Logger +var logFile *os.File +var logWriter *syncWriter +var logCtx context.Context +var logCancel context.CancelFunc + +// syncWriter écrit de manière synchrone (pas de buffer) +type syncWriter struct { + file *os.File + mutex sync.Mutex +} + +func (w *syncWriter) Write(p []byte) (n int, err error) { + w.mutex.Lock() + defer w.mutex.Unlock() + + n, err = w.file.Write(p) + if err == nil { + w.file.Sync() // Force l'écriture immédiate sur disque + } + return n, err +} func NewLog() *log.Logger { + // ✅ Vérifier que Cfg existe + if Cfg == nil { + panic("Config not initialized! Call NewConfig() before NewLog()") + } + + // ✅ Chemin du log à côté de l'exe + exe, _ := os.Executable() + exePath := filepath.Dir(exe) + logPath := filepath.Join(exePath, "flexradio.log") + if Cfg.General.DeleteLogFileAtStart { - if _, err := os.Stat("flexradio.log"); err == nil { - os.Remove("flexradio.log") + if _, err := os.Stat(logPath); err == nil { + os.Remove(logPath) } } + logCtx, logCancel = context.WithCancel(context.Background()) + var w io.Writer + if Cfg.General.LogToFile { - f, err := os.OpenFile("flexradio.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { - panic(err) + panic(fmt.Sprintf("Cannot open log file %s: %v", logPath, err)) + } + + logFile = f + logWriter = &syncWriter{file: f} + + // ✅ IMPORTANT: Vérifier si Stdout est disponible (mode console vs GUI) + if isConsoleAvailable() { + // Mode console : log vers fichier ET console + w = io.MultiWriter(os.Stdout, logWriter) + } else { + // Mode GUI (windowsgui) : log SEULEMENT vers fichier + w = logWriter } - w = io.MultiWriter(os.Stdout, f) } else { - w = io.Writer(os.Stdout) + // Log uniquement vers console (si disponible) + if isConsoleAvailable() { + w = os.Stdout + } else { + // Pas de console, pas de log fichier -> log vers null + w = io.Discard + } } Log = &log.Logger{ Out: w, Formatter: &prefixed.TextFormatter{ - DisableColors: false, - TimestampFormat: "02-01-2006 15:04:05", - FullTimestamp: true, - ForceFormatting: true, + DisableColors: !isConsoleAvailable(), + TimestampFormat: "02-01-2006 15:04:05", + FullTimestamp: true, + ForceFormatting: true, + DisableSorting: true, // ✅ Ajoute + QuoteEmptyFields: true, // ✅ Ajoute + SpacePadding: 0, // ✅ Ajoute (pas d'espace) }, + Hooks: make(log.LevelHooks), } if Cfg.General.LogLevel == "DEBUG" { @@ -49,5 +109,55 @@ func NewLog() *log.Logger { Log.Level = log.InfoLevel } + logBuffer = NewLogBuffer(500) // Garde les 500 derniers logs + // Log.AddHook(&LogHook{buffer: logBuffer}) + + // ✅ Premier vrai log + Log.Infof("Logger initialized - Level: %s, ToFile: %v, LogPath: %s", + Cfg.General.LogLevel, Cfg.General.LogToFile, logPath) + return Log } + +func InitLogHook() { + if logBuffer == nil { + logBuffer = NewLogBuffer(500) + } + + Log.AddHook(&LogHook{buffer: logBuffer}) + Log.Info("Log hook initialized and broadcasting enabled") +} + +// ✅ Détecter si on a une console (fonctionne sur Windows) +func isConsoleAvailable() bool { + // Si Stdout est nil ou invalide, on n'a pas de console + stat, err := os.Stdout.Stat() + if err != nil { + return false + } + // Si c'est un char device, on a une console + return (stat.Mode() & os.ModeCharDevice) != 0 +} + +// ✅ Fonction pour fermer proprement le log +func CloseLog() { + if Log != nil { + Log.Info("Closing log file...") + } + + if logCancel != nil { + logCancel() + } + + time.Sleep(200 * time.Millisecond) // Donne le temps d'écrire + + if logWriter != nil { + logWriter.mutex.Lock() + if logFile != nil { + logFile.Sync() + logFile.Close() + logFile = nil + } + logWriter.mutex.Unlock() + } +} diff --git a/loghook.go b/loghook.go new file mode 100644 index 0000000..2129005 --- /dev/null +++ b/loghook.go @@ -0,0 +1,80 @@ +package main + +import ( + "sync" + + log "github.com/sirupsen/logrus" +) + +// LogBuffer garde les derniers logs en mémoire +type LogBuffer struct { + entries []LogEntry + maxSize int + mutex sync.RWMutex +} + +type LogEntry struct { + Timestamp string `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` +} + +var logBuffer *LogBuffer + +func NewLogBuffer(maxSize int) *LogBuffer { + return &LogBuffer{ + entries: make([]LogEntry, 0, maxSize), + maxSize: maxSize, + } +} + +func (lb *LogBuffer) Add(entry LogEntry) { + lb.mutex.Lock() + defer lb.mutex.Unlock() + + lb.entries = append(lb.entries, entry) + + // Garder seulement les N derniers + if len(lb.entries) > lb.maxSize { + lb.entries = lb.entries[1:] + } +} + +func (lb *LogBuffer) GetAll() []LogEntry { + lb.mutex.RLock() + defer lb.mutex.RUnlock() + + // Retourner une copie + result := make([]LogEntry, len(lb.entries)) + copy(result, lb.entries) + return result +} + +// Hook pour capturer les logs +type LogHook struct { + buffer *LogBuffer +} + +func (h *LogHook) Levels() []log.Level { + return log.AllLevels +} + +func (h *LogHook) Fire(entry *log.Entry) error { + logEntry := LogEntry{ + Timestamp: entry.Time.Format("02-01-2006 15:04:05"), + Level: entry.Level.String(), + Message: entry.Message, + } + + h.buffer.Add(logEntry) + + // Broadcaster vers les clients WebSocket connectés + if httpServerInstance != nil { + httpServerInstance.broadcast <- WSMessage{ + Type: "appLog", + Data: logEntry, + } + } + + return nil +} diff --git a/main.go b/main.go index fdc16f3..d1ca388 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,35 @@ func ParseFlags() (string, error) { return configPath, nil } +func GracefulShutdown(tcpClient *TCPClient, tcpServer *TCPServer, flexClient *FlexClient, flexRepo *FlexDXClusterRepository, contactRepo *Log4OMContactsRepository) { + Log.Info("Starting graceful shutdown...") + + // Fermer les clients + if tcpClient != nil { + tcpClient.Close() + } + if flexClient != nil { + flexClient.Close() + } + + // Fermer les serveurs + if tcpServer != nil { + // tcpServer.Close() si tu as une méthode close + } + + // Fermer les bases de données + if flexRepo != nil && flexRepo.db != nil { + flexRepo.db.Close() + } + if contactRepo != nil && contactRepo.db != nil { + contactRepo.db.Close() + } + + // ✅ Fermer le log en dernier + Log.Info("Shutdown complete") + CloseLog() +} + func main() { // Generate our config based on the config supplied @@ -42,17 +71,18 @@ func main() { log.Fatal(err) } - cfg := NewConfig(cfgPath) + NewConfig(cfgPath) log := NewLog() + defer CloseLog() log.Info("Running FlexDXCluster version 2.1") - log.Infof("Callsign: %s", cfg.General.Callsign) + log.Infof("Callsign: %s", Cfg.General.Callsign) DeleteDatabase("./flex.sqlite", log) - log.Debugf("Gotify Push Enabled: %v", cfg.Gotify.Enable) - if cfg.Gotify.Enable { - log.Debugf("Gotify Push NewDXCC: %v - NewBand: %v - NewMode: %v - NewBandAndMode: %v", cfg.Gotify.NewDXCC, cfg.Gotify.NewBand, cfg.Gotify.NewMode, cfg.Gotify.NewBandAndMode) + log.Debugf("Gotify Push Enabled: %v", Cfg.Gotify.Enable) + if Cfg.Gotify.Enable { + log.Debugf("Gotify Push NewDXCC: %v - NewBand: %v - NewMode: %v - NewBandAndMode: %v", Cfg.Gotify.NewDXCC, Cfg.Gotify.NewBand, Cfg.Gotify.NewMode, Cfg.Gotify.NewBandAndMode) } // Load country.xml to get all the DXCC number @@ -64,7 +94,7 @@ func main() { defer fRepo.db.Close() // Database connection to Log4OM - cRepo := NewLog4OMContactsRepository(cfg.SQLite.SQLitePath) + cRepo := NewLog4OMContactsRepository(Cfg.SQLite.SQLitePath) defer cRepo.db.Close() contacts := cRepo.CountEntries() log.Infof("Log4OM Database Contains %v Contacts", contacts) @@ -73,12 +103,13 @@ func main() { SpotChanToHTTPServer := make(chan TelnetSpot, 100) // Initialize servers and clients - TCPServer := NewTCPServer(cfg.TelnetServer.Host, cfg.TelnetServer.Port) + TCPServer := NewTCPServer(Cfg.TelnetServer.Host, Cfg.TelnetServer.Port) TCPClient := NewTCPClient(TCPServer, Countries, cRepo, SpotChanToHTTPServer) FlexClient := NewFlexClient(*fRepo, TCPServer, nil, nil) // Initialize HTTP Server for Dashboard HTTPServer := NewHTTPServer(fRepo, cRepo, TCPServer, TCPClient, FlexClient, "8080") + InitLogHook() FlexClient.HTTPServer = HTTPServer @@ -92,8 +123,8 @@ func main() { go TCPServer.StartServer() go HTTPServer.Start() - log.Infof("Telnet Server: %s:%s", cfg.TelnetServer.Host, cfg.TelnetServer.Port) - log.Infof("Cluster: %s:%s", cfg.Cluster.Server, cfg.Cluster.Port) + log.Infof("Telnet Server: %s:%s", Cfg.TelnetServer.Host, Cfg.TelnetServer.Port) + log.Infof("Cluster: %s:%s", Cfg.Cluster.Server, Cfg.Cluster.Port) CheckSignal(TCPClient, TCPServer, FlexClient, fRepo, cRepo) diff --git a/spot.go b/spot.go index f65c2a9..6cfe885 100644 --- a/spot.go +++ b/spot.go @@ -32,123 +32,136 @@ func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan Te match := re.FindStringSubmatch(spotRaw) - if len(match) != 0 { - spot := TelnetSpot{ - DX: match[3], - Spotter: match[1], - Frequency: match[2], - Mode: match[4], - Comment: strings.Trim(match[5], " "), - Time: match[6], - } - - DXCC := GetDXCC(spot.DX, Countries) - spot.DXCC = DXCC.DXCC - spot.CountryName = DXCC.CountryName - - if spot.DXCC == "" { - Log.Errorf("Could not identify the DXCC for %s", spot.DX) - return - } - - spot.GetBand() - spot.GuessMode() - spot.CallsignWorked = false - spot.NewBand = false - spot.NewMode = false - spot.NewDXCC = false - spot.NewSlot = false - - contactsChan := make(chan []Contact) - contactsModeChan := make(chan []Contact) - contactsModeBandChan := make(chan []Contact) - contactsBandChan := make(chan []Contact) - contactsCallChan := make(chan []Contact) - - wg := new(sync.WaitGroup) - wg.Add(5) - - go contactRepo.ListByCountry(spot.DXCC, contactsChan, wg) - contacts := <-contactsChan - - go contactRepo.ListByCountryMode(spot.DXCC, spot.Mode, contactsModeChan, wg) - contactsMode := <-contactsModeChan - - go contactRepo.ListByCountryBand(spot.DXCC, spot.Band, contactsBandChan, wg) - contactsBand := <-contactsBandChan - - go contactRepo.ListByCallSign(spot.DX, spot.Band, spot.Mode, contactsCallChan, wg) - contactsCall := <-contactsCallChan - - go contactRepo.ListByCountryModeBand(spot.DXCC, spot.Band, spot.Mode, contactsModeBandChan, wg) - contactsModeBand := <-contactsModeBandChan - - wg.Wait() - - if len(contacts) == 0 { - spot.NewDXCC = true - } - - if len(contactsMode) == 0 { - spot.NewMode = true - } - if len(contactsBand) == 0 { - spot.NewBand = true - } - - if len(contactsModeBand) == 0 && !spot.NewDXCC && !spot.NewBand && !spot.NewMode { - spot.NewSlot = true - } - - if len(contactsCall) > 0 { - spot.CallsignWorked = true - } - - // Envoyer TOUJOURS le spot vers le processeur principal (base de données + HTTP) - // Ce canal est maintenant géré par une goroutine dans main.go - select { - case SpotChanToHTTPServer <- spot: - // Spot envoyé avec succès - default: - Log.Warn("SpotChanToHTTPServer is full, spot may be lost") - } - - // Logging des spots - if spot.NewDXCC { - Log.Debugf("(** New DXCC **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s", - spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC) - } - - if !spot.NewDXCC && spot.NewBand && spot.NewMode { - Log.Debugf("(** New Band/Mode **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s", - spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC) - } - - if !spot.NewDXCC && spot.NewBand && !spot.NewMode { - Log.Debugf("(** New Band **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s", - spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC) - } - - if !spot.NewDXCC && !spot.NewBand && spot.NewMode && spot.Mode != "" { - Log.Debugf("(** New Mode **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s", - spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC) - } - - if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && spot.NewSlot && spot.Mode != "" { - Log.Debugf("(** New Slot **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s", - spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC) - } - - if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && spot.CallsignWorked { - Log.Debugf("(** Worked **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s", - spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC) - } - - if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && !spot.CallsignWorked { - Log.Debugf("DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s", - spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC) - } + if len(match) == 0 { + IncrementSpotsRejected() + Log.Warnf("❌ Regex no match: %s", spotRaw) + return } + + spot := TelnetSpot{ + DX: match[3], + Spotter: match[1], + Frequency: match[2], + Mode: match[4], + Comment: strings.Trim(match[5], " "), + Time: match[6], + } + + DXCC := GetDXCC(spot.DX, Countries) + spot.DXCC = DXCC.DXCC + spot.CountryName = DXCC.CountryName + + if spot.DXCC == "" { + IncrementSpotsRejected() + Log.Warnf("❌ DXCC not found: %s", spot.DX) + return + } + + spot.GetBand() + spot.GuessMode(spotRaw) + spot.CallsignWorked = false + spot.NewBand = false + spot.NewMode = false + spot.NewDXCC = false + spot.NewSlot = false + + contactsChan := make(chan []Contact) + contactsModeChan := make(chan []Contact) + contactsModeBandChan := make(chan []Contact) + contactsBandChan := make(chan []Contact) + contactsCallChan := make(chan []Contact) + + wg := new(sync.WaitGroup) + wg.Add(5) + + go contactRepo.ListByCountry(spot.DXCC, contactsChan, wg) + contacts := <-contactsChan + + go contactRepo.ListByCountryMode(spot.DXCC, spot.Mode, contactsModeChan, wg) + contactsMode := <-contactsModeChan + + go contactRepo.ListByCountryBand(spot.DXCC, spot.Band, contactsBandChan, wg) + contactsBand := <-contactsBandChan + + go contactRepo.ListByCallSign(spot.DX, spot.Band, spot.Mode, contactsCallChan, wg) + contactsCall := <-contactsCallChan + + go contactRepo.ListByCountryModeBand(spot.DXCC, spot.Band, spot.Mode, contactsModeBandChan, wg) + contactsModeBand := <-contactsModeBandChan + + wg.Wait() + + // ✅ Déterminer le statut + if len(contacts) == 0 { + spot.NewDXCC = true + } + if len(contactsMode) == 0 { + spot.NewMode = true + } + if len(contactsBand) == 0 { + spot.NewBand = true + } + if len(contactsModeBand) == 0 && !spot.NewDXCC && !spot.NewBand && !spot.NewMode { + spot.NewSlot = true + } + if len(contactsCall) > 0 { + spot.CallsignWorked = true + } + + // ✅ Envoyer le spot + select { + case SpotChanToHTTPServer <- spot: + IncrementSpotsProcessed() + default: + IncrementSpotsRejected() + Log.Errorf("❌ Spot dropped (channel full): %s @ %s", spot.DX, spot.Frequency) + return + } + + // ✅ LOGS CONCIS ET ADAPTES + statusIcon := "" + statusText := "" + + if spot.NewDXCC { + statusIcon = "🆕" + statusText = "NEW DXCC" + } else if spot.NewBand && spot.NewMode { + statusIcon = "📻" + statusText = "NEW BAND+MODE" + } else if spot.NewBand { + statusIcon = "📡" + statusText = "NEW BAND" + } else if spot.NewMode { + statusIcon = "🔧" + statusText = "NEW MODE" + } else if spot.NewSlot { + statusIcon = "✨" + statusText = "NEW SLOT" + } else if spot.CallsignWorked { + statusIcon = "✓" + statusText = "WORKED" + } else { + statusIcon = "·" + statusText = "SPOT" + } + + // ✅ Log unique et concis + Log.Debugf("%s [%s] %s on %.1f kHz (%s %s) - %s @ %s", + statusIcon, + statusText, + spot.DX, + mustParseFloat(spot.Frequency), + spot.Band, + spot.Mode, + spot.CountryName, + spot.Time, + ) +} + +// ✅ Helper pour convertir la fréquence +func mustParseFloat(s string) float64 { + f, _ := strconv.ParseFloat(s, 64) + return f } func (spot *TelnetSpot) GetBand() { @@ -216,189 +229,231 @@ func (spot *TelnetSpot) GetBand() { } } -func (spot *TelnetSpot) GuessMode() { +func (spot *TelnetSpot) GuessMode(rawSpot string) { + // ✅ D'ABORD : Chercher le mode dans le commentaire + if spot.Mode == "" { + spot.Mode = extractModeFromComment(spot.Comment) + if spot.Mode != "" { + Log.Debugf("Mode extracted from comment: %s", spot.Mode) + } + } + + // ✅ Normaliser SSB avant de deviner + if spot.Mode == "SSB" { + if spot.Band == "10M" || spot.Band == "12M" || spot.Band == "6M" || spot.Band == "15M" || spot.Band == "17M" || spot.Band == "20M" { + spot.Mode = "USB" + } else { + spot.Mode = "LSB" + } + Log.Debugf("Converted SSB to %s for band %s", spot.Mode, spot.Band) + return + } + + // ✅ Si pas de mode, deviner depuis la fréquence if spot.Mode == "" { freqInt, err := strconv.ParseFloat(spot.Frequency, 32) + Log.Debugf("No mode specified in spot, will guess from frequency %v with spot: %s", freqInt, strings.TrimSpace(rawSpot)) if err != nil { - Log.Errorf("could not convert frequency string in float64:", err) + Log.Errorf("could not convert frequency string to float: %v", err) + return } switch spot.Band { - case "160M": - if freqInt >= 1800 && freqInt <= 1840 { + case "160M": // 1.800 - 2.000 MHz + if freqInt < 1838 { spot.Mode = "CW" - } - if freqInt >= 1840 && freqInt <= 1844 { + } else if freqInt < 1843 { spot.Mode = "FT8" - } - case "80M": - if freqInt >= 3500 && freqInt < 3568 { - spot.Mode = "CW" - } - if freqInt >= 3568 && freqInt < 3573 { - spot.Mode = "FT4" - } - if freqInt >= 3573 && freqInt < 3580 { - spot.Mode = "FT8" - } - if freqInt >= 3580 && freqInt < 3600 { - spot.Mode = "CW" - } - if freqInt >= 3600 && freqInt <= 3800 { - spot.Mode = "LSB" - } - case "60M": - if freqInt >= 5351.5 && freqInt < 5354 { - spot.Mode = "CW" - } - if freqInt >= 5354 && freqInt < 5366 { - spot.Mode = "LSB" - } - if freqInt >= 5366 && freqInt <= 5266.5 { - spot.Mode = "FT8" - } - case "40M": - if freqInt >= 7000 && freqInt < 7045.5 { - spot.Mode = "CW" - } - if freqInt >= 7045.5 && freqInt < 7048.5 { - spot.Mode = "FT4" - } - if freqInt >= 7048.5 && freqInt < 7074 { - spot.Mode = "CW" - } - if freqInt >= 7074 && freqInt < 7078 { - spot.Mode = "FT8" - } - if freqInt >= 7078 && freqInt <= 7300 { - spot.Mode = "LSB" - } - case "30M": - if freqInt >= 10100 && freqInt < 10130 { - spot.Mode = "CW" - } - if freqInt >= 10130 && freqInt < 10140 { - spot.Mode = "FT8" - } - if freqInt >= 10140 && freqInt <= 10150 { - spot.Mode = "FT4" - } - case "20M": - if freqInt >= 14000 && freqInt < 14074 { - spot.Mode = "CW" - } - if freqInt >= 14074 && freqInt < 14078 { - spot.Mode = "FT8" - } - if freqInt >= 14078 && freqInt < 14083 { - spot.Mode = "FT4" - } - if freqInt >= 14083 && freqInt < 14119 { - spot.Mode = "FT8" - } - if freqInt >= 14119 && freqInt < 14350 { - spot.Mode = "USB" - } - - case "17M": - if freqInt >= 18068 && freqInt < 18090 { - spot.Mode = "CW" - } - if freqInt >= 18090 && freqInt < 18104 { - spot.Mode = "FT8" - } - if freqInt >= 18104 && freqInt < 18108 { - spot.Mode = "FT4" - } - if freqInt >= 18108 && freqInt <= 18168 { - spot.Mode = "USB" - } - - case "15M": - if freqInt >= 21000 && freqInt < 21074 { - spot.Mode = "CW" - } - if freqInt >= 21074 && freqInt < 21100 { - spot.Mode = "FT8" - } - if freqInt >= 21100 && freqInt < 21140 { - spot.Mode = "RTTY" - } - if freqInt >= 21140 && freqInt < 21144 { - spot.Mode = "FT4" - } - if freqInt >= 21144 && freqInt <= 21450 { - spot.Mode = "USB" - } - - case "12M": - if freqInt >= 24890 && freqInt < 24910 { - spot.Mode = "CW" - } - if freqInt >= 24910 && freqInt < 24919 { - spot.Mode = "FT8" - } - if freqInt >= 24919 && freqInt < 24922 { - spot.Mode = "FT4" - } - if freqInt >= 24922 && freqInt < 24930 { - spot.Mode = "RTTY" - } - if freqInt >= 24930 && freqInt <= 24990 { - spot.Mode = "USB" - } - - case "10M": - if freqInt >= 28000 && freqInt < 28070 { - spot.Mode = "CW" - } - if freqInt >= 28070 && freqInt < 28080 { - spot.Mode = "FT8" - } - if freqInt >= 28080 && freqInt < 28100 { - spot.Mode = "RTTY" - } - if freqInt >= 28100 && freqInt < 28180 { - spot.Mode = "CW" - } - if freqInt >= 28180 && freqInt < 28190 { - spot.Mode = "FT4" - } - if freqInt >= 28190 && freqInt < 29000 { - spot.Mode = "USB" - } - if freqInt >= 29000 && freqInt <= 29700 { - spot.Mode = "FM" - } - case "6M": - if freqInt >= 50000 && freqInt < 50100 { - spot.Mode = "CW" - } - if freqInt >= 50100 && freqInt < 50313 { - spot.Mode = "USB" - } - if freqInt >= 50313 && freqInt < 50320 { - spot.Mode = "FT8" - } - if freqInt >= 50320 && freqInt < 50400 { - spot.Mode = "USB" - } - if freqInt >= 50400 && freqInt < +52000 { - spot.Mode = "FM" - } - } - } else { - spot.Mode = strings.ToUpper(spot.Mode) - if spot.Mode == "SSB" { - if spot.Band == "10M" || spot.Band == "12M" || spot.Band == "6M" || spot.Band == "15M" || spot.Band == "17M" || spot.Band == "20M" { - spot.Mode = "USB" } else { spot.Mode = "LSB" } + + case "80M": // 3.500 - 4.000 MHz + if freqInt < 3570 { + spot.Mode = "CW" + } else if freqInt < 3575 { + spot.Mode = "FT4" + } else if freqInt < 3578 { + spot.Mode = "FT8" + } else if freqInt < 3590 { + spot.Mode = "RTTY" + } else { + spot.Mode = "LSB" + } + + case "60M": // 5.330 - 5.405 MHz + if freqInt < 5357 { + spot.Mode = "CW" + } else if freqInt < 5359 { + spot.Mode = "FT8" + } else { + spot.Mode = "USB" + } + + case "40M": // 7.000 - 7.300 MHz + if freqInt < 7040 { + spot.Mode = "CW" + } else if freqInt < 7047 { + spot.Mode = "RTTY" + } else if freqInt < 7050 { + spot.Mode = "FT4" + } else if freqInt < 7080 { + spot.Mode = "FT8" // ✅ 7.056 = FT8 + } else if freqInt < 7125 { + spot.Mode = "RTTY" // ✅ 7.112 = RTTY + } else { + spot.Mode = "LSB" + } + + case "30M": // 10.100 - 10.150 MHz (CW/Digital seulement) + if freqInt < 10130 { + spot.Mode = "CW" + } else if freqInt < 10142 { + spot.Mode = "FT8" + } else { + spot.Mode = "FT4" + } + + case "20M": // 14.000 - 14.350 MHz + if freqInt < 14070 { + spot.Mode = "CW" + } else if freqInt < 14078 { + spot.Mode = "FT8" + } else if freqInt < 14083 { + spot.Mode = "FT4" + } else if freqInt < 14112 { + spot.Mode = "RTTY" + } else { + spot.Mode = "USB" + } + + case "17M": // 18.068 - 18.168 MHz + if freqInt < 18090 { + spot.Mode = "CW" + } else if freqInt < 18104 { + spot.Mode = "FT8" + } else if freqInt < 18106 { + spot.Mode = "FT4" + } else if freqInt < 18110 { + spot.Mode = "RTTY" + } else { + spot.Mode = "USB" + } + + case "15M": // 21.000 - 21.450 MHz + if freqInt < 21070 { + spot.Mode = "CW" + } else if freqInt < 21078 { + spot.Mode = "FT8" + } else if freqInt < 21120 { + spot.Mode = "RTTY" + } else if freqInt < 21143 { + spot.Mode = "FT4" + } else { + spot.Mode = "USB" + } + + case "12M": // 24.890 - 24.990 MHz + if freqInt < 24910 { + spot.Mode = "CW" // ✅ 24.896 = CW + } else if freqInt < 24918 { + spot.Mode = "FT8" + } else if freqInt < 24922 { + spot.Mode = "FT4" + } else if freqInt < 24930 { + spot.Mode = "RTTY" + } else { + spot.Mode = "USB" + } + + case "10M": // 28.000 - 29.700 MHz + if freqInt < 28070 { + spot.Mode = "CW" + } else if freqInt < 28095 { + spot.Mode = "FT8" + } else if freqInt < 28179 { + spot.Mode = "RTTY" + } else if freqInt < 28190 { + spot.Mode = "FT4" + } else if freqInt < 29000 { + spot.Mode = "USB" + } else { + spot.Mode = "FM" + } + + case "6M": // 50.000 - 54.000 MHz + if freqInt < 50100 { + spot.Mode = "CW" + } else if freqInt < 50313 { + spot.Mode = "USB" // ✅ DX Window + général + } else if freqInt < 50318 { + spot.Mode = "FT8" // ✅ 50.313-50.318 + } else if freqInt < 50323 { + spot.Mode = "FT4" // ✅ 50.318-50.323 + } else if freqInt < 51000 { + spot.Mode = "USB" // ✅ Retour à USB + } else { + spot.Mode = "FM" + } + + default: + // ✅ Bande inconnue + if freqInt < 10.0 { + spot.Mode = "LSB" + } else { + spot.Mode = "USB" + } + } + + if spot.Mode != "" { + Log.Debugf("✅ Guessed mode %s for %s on %s MHz (band %s)", spot.Mode, spot.DX, spot.Frequency, spot.Band) + } else { + Log.Warnf("❌ Could not guess mode for %s on %s MHz (band %s), raw spot: %s", spot.DX, spot.Frequency, spot.Band, rawSpot) + } + } else { + spot.Mode = strings.ToUpper(spot.Mode) + } +} + +// ✅ Extraire le mode depuis le commentaire +func extractModeFromComment(comment string) string { + commentUpper := strings.ToUpper(comment) + + // ✅ 1. Détecter FT8/FT4 avec leurs patterns typiques (dB + Hz) + if strings.Contains(commentUpper, "FT8") || + (strings.Contains(commentUpper, "DB") && strings.Contains(commentUpper, "HZ")) { + return "FT8" + } + + if strings.Contains(commentUpper, "FT4") { + return "FT4" + } + + // ✅ 2. Détecter CW avec WPM (Words Per Minute) + if strings.Contains(commentUpper, "WPM") || strings.Contains(commentUpper, " CW ") || + strings.HasSuffix(commentUpper, "CW") || strings.HasPrefix(commentUpper, "CW ") { + return "CW" + } + + // ✅ 3. Autres modes digitaux + digitalModes := []string{"RTTY", "PSK31", "PSK63", "PSK", "MFSK", "OLIVIA", "CONTESTIA", "JT65", "JT9"} + for _, mode := range digitalModes { + if strings.Contains(commentUpper, mode) { + return mode } } - if spot.Mode == "" { - Log.Errorf("Could not identify mode for %s on %s", spot.DX, spot.Frequency) + // ✅ 4. Modes voice + voiceModes := []string{"USB", "LSB", "SSB", "FM", "AM"} + for _, mode := range voiceModes { + // Chercher le mode comme mot complet (pas dans "SSBC" par exemple) + if strings.Contains(commentUpper, " "+mode+" ") || + strings.HasPrefix(commentUpper, mode+" ") || + strings.HasSuffix(commentUpper, " "+mode) || + commentUpper == mode { + return mode + } } + + return "" } diff --git a/spotprocessor.go b/spotprocessor.go index 7dc2433..4874943 100644 --- a/spotprocessor.go +++ b/spotprocessor.go @@ -78,6 +78,7 @@ func (sp *SpotProcessor) processSpot(spot TelnetSpot) { DXCC: spot.DXCC, } + flexSpot.OriginalComment = spot.Comment flexSpot.Comment = flexSpot.Comment + " [" + flexSpot.Mode + "] [" + flexSpot.SpotterCallsign + "] [" + flexSpot.UTCTime + "]" if sp.HTTPServer != nil && sp.HTTPServer.Watchlist != nil { diff --git a/stats.go b/stats.go new file mode 100644 index 0000000..4688c9e --- /dev/null +++ b/stats.go @@ -0,0 +1,45 @@ +package main + +import "sync" + +var ( + spotsReceived int64 + spotsProcessed int64 + spotsRejected int64 + spotsMutex sync.RWMutex +) + +func IncrementSpotsReceived() { + spotsMutex.Lock() + spotsReceived++ + spotsMutex.Unlock() +} + +func IncrementSpotsProcessed() { + spotsMutex.Lock() + spotsProcessed++ + spotsMutex.Unlock() +} + +func IncrementSpotsRejected() { + spotsMutex.Lock() + spotsRejected++ + spotsMutex.Unlock() +} + +func GetSpotStats() (int64, int64, int64) { + spotsMutex.RLock() + defer spotsMutex.RUnlock() + return spotsReceived, spotsProcessed, spotsRejected +} + +func GetSpotSuccessRate() float64 { + spotsMutex.RLock() + defer spotsMutex.RUnlock() + + if spotsReceived == 0 { + return 0.0 + } + + return float64(spotsProcessed) / float64(spotsReceived) * 100.0 +} diff --git a/utils.go b/utils.go index 915748d..ef0cc10 100644 --- a/utils.go +++ b/utils.go @@ -32,31 +32,14 @@ func FreqHztoMhz(freq string) string { return strconv.FormatFloat(frequency, 'f', 6, 64) } -func CheckSignal(TCPClient *TCPClient, TCPServer *TCPServer, FlexClient *FlexClient, fRepo *FlexDXClusterRepository, cRepo *Log4OMContactsRepository) { +func CheckSignal(tcpClient *TCPClient, tcpServer *TCPServer, flexClient *FlexClient, flexRepo *FlexDXClusterRepository, contactRepo *Log4OMContactsRepository) { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, os.Interrupt, syscall.SIGTERM) - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) + <-sigchan - // Gracely closing all connextions if signal is received - for sig := range sigCh { - Log.Infof("received signal: %v, shutting down all connections.", sig) - - TCPClient.Close() - TCPServer.Conn.Close() - FlexClient.Conn.Close() - - if err := fRepo.db.Close(); err != nil { - Log.Error("failed to close the database connection properly") - os.Exit(1) - } - - if err := cRepo.db.Close(); err != nil { - Log.Error("failed to close Log4OM database connection properly") - os.Exit(1) - } - - os.Exit(0) - } + GracefulShutdown(tcpClient, tcpServer, flexClient, flexRepo, contactRepo) + os.Exit(0) } func SendUDPMessage(data []byte) { diff --git a/watchlist.json b/watchlist.json index fb5145a..bc22def 100644 --- a/watchlist.json +++ b/watchlist.json @@ -1,10 +1,37 @@ [ { - "callsign": "H44MS", + "callsign": "3B8M", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:16:49.1572859+02:00", + "addedAt": "2025-10-18T17:18:32.6851135+02:00", + "spotCount": 0, + "playSound": true + }, + { + "callsign": "E6AD", + "notes": "", + "lastSeen": "2025-10-19T10:11:57.4553495+02:00", + "lastSeenStr": "Just now", + "addedAt": "2025-10-18T17:17:40.8765179+02:00", + "spotCount": 198, + "playSound": true + }, + { + "callsign": "TZ4AM", + "notes": "", + "lastSeen": "2025-10-18T23:16:45.8032011+02:00", + "lastSeenStr": "10 hours ago", + "addedAt": "2025-10-18T17:19:00.3154177+02:00", + "spotCount": 22, + "playSound": true + }, + { + "callsign": "5J0EA", + "notes": "", + "lastSeen": "0001-01-01T00:00:00Z", + "lastSeenStr": "Never", + "addedAt": "2025-10-18T17:17:51.0758741+02:00", "spotCount": 0, "playSound": true }, @@ -18,47 +45,47 @@ "playSound": true }, { - "callsign": "C5LT", + "callsign": "C21TS", + "notes": "", + "lastSeen": "2025-10-18T18:50:28.7708075+02:00", + "lastSeenStr": "15 hours ago", + "addedAt": "2025-10-18T17:18:21.7895474+02:00", + "spotCount": 9, + "playSound": true + }, + { + "callsign": "E51MWA", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:07.2442738+02:00", + "addedAt": "2025-10-18T17:17:43.6895454+02:00", "spotCount": 0, "playSound": true }, { - "callsign": "PJ6Y", - "notes": "", - "lastSeen": "2025-10-18T18:44:30.0286129+02:00", - "lastSeenStr": "Just now", - "addedAt": "2025-10-18T17:17:47.7237081+02:00", - "spotCount": 20, - "playSound": true - }, - { - "callsign": "PY0FB", + "callsign": "ZL7IO", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:17:24.3843986+02:00", + "addedAt": "2025-10-18T17:17:30.7153757+02:00", "spotCount": 0, "playSound": true }, { - "callsign": "5H3MB", + "callsign": "XV9", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:42.8402097+02:00", + "addedAt": "2025-10-18T17:18:24.9155327+02:00", "spotCount": 0, "playSound": true }, { - "callsign": "YJ0CA", + "callsign": "9L8MD", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:17:33.3921665+02:00", + "addedAt": "2025-10-18T17:18:56.7896868+02:00", "spotCount": 0, "playSound": true }, @@ -66,35 +93,17 @@ "callsign": "TJ1GD", "notes": "", "lastSeen": "2025-10-18T18:45:59.6232796+02:00", - "lastSeenStr": "Just now", + "lastSeenStr": "15 hours ago", "addedAt": "2025-10-18T17:18:27.6004027+02:00", "spotCount": 10, "playSound": true }, { - "callsign": "C8K", + "callsign": "V85NPV", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:39.8627992+02:00", - "spotCount": 0, - "playSound": true - }, - { - "callsign": "VP2M", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:17:57.308717+02:00", - "spotCount": 0, - "playSound": true - }, - { - "callsign": "XT2AW", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:17:27.3839089+02:00", + "addedAt": "2025-10-18T17:18:15.8781583+02:00", "spotCount": 0, "playSound": true }, @@ -107,33 +116,105 @@ "spotCount": 0, "playSound": true }, + { + "callsign": "YI1MB", + "notes": "", + "lastSeen": "0001-01-01T00:00:00Z", + "lastSeenStr": "Never", + "addedAt": "2025-10-18T17:18:18.825584+02:00", + "spotCount": 0, + "playSound": true + }, + { + "callsign": "YJ0CA", + "notes": "", + "lastSeen": "0001-01-01T00:00:00Z", + "lastSeenStr": "Never", + "addedAt": "2025-10-18T17:17:33.3921665+02:00", + "spotCount": 0, + "playSound": true + }, { "callsign": "FW5K", "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", + "lastSeen": "2025-10-19T10:11:59.0591627+02:00", + "lastSeenStr": "Just now", "addedAt": "2025-10-18T17:17:37.9061157+02:00", + "spotCount": 45, + "playSound": true + }, + { + "callsign": "VP2M", + "notes": "", + "lastSeen": "0001-01-01T00:00:00Z", + "lastSeenStr": "Never", + "addedAt": "2025-10-18T17:17:57.308717+02:00", "spotCount": 0, "playSound": true }, { - "callsign": "E51MWA", + "callsign": "H44MS", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:17:43.6895454+02:00", + "addedAt": "2025-10-18T17:16:49.1572859+02:00", "spotCount": 0, "playSound": true }, { - "callsign": "3B8M", + "callsign": "VP8LP", + "notes": "", + "lastSeen": "2025-10-19T02:44:48.7228962+02:00", + "lastSeenStr": "7 hours ago", + "addedAt": "2025-10-18T17:18:49.0576187+02:00", + "spotCount": 4, + "playSound": true + }, + { + "callsign": "C5LT", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:32.6851135+02:00", + "addedAt": "2025-10-18T17:18:07.2442738+02:00", "spotCount": 0, "playSound": true }, + { + "callsign": "XT2AW", + "notes": "", + "lastSeen": "2025-10-19T10:00:12.7622002+02:00", + "lastSeenStr": "10 minutes ago", + "addedAt": "2025-10-18T17:17:27.3839089+02:00", + "spotCount": 3, + "playSound": true + }, + { + "callsign": "C8K", + "notes": "", + "lastSeen": "0001-01-01T00:00:00Z", + "lastSeenStr": "Never", + "addedAt": "2025-10-18T17:18:39.8627992+02:00", + "spotCount": 0, + "playSound": true + }, + { + "callsign": "EL2BG", + "notes": "", + "lastSeen": "2025-10-19T10:03:29.0397937+02:00", + "lastSeenStr": "7 minutes ago", + "addedAt": "2025-10-18T17:18:10.2000017+02:00", + "spotCount": 8, + "playSound": true + }, + { + "callsign": "PJ6Y", + "notes": "", + "lastSeen": "2025-10-19T09:46:21.9937557+02:00", + "lastSeenStr": "24 minutes ago", + "addedAt": "2025-10-18T17:17:47.7237081+02:00", + "spotCount": 219, + "playSound": true + }, { "callsign": "Z66IPA", "notes": "", @@ -143,85 +224,13 @@ "spotCount": 0, "playSound": true }, - { - "callsign": "C21TS", - "notes": "", - "lastSeen": "2025-10-18T18:50:28.7708075+02:00", - "lastSeenStr": "Just now", - "addedAt": "2025-10-18T17:18:21.7895474+02:00", - "spotCount": 9, - "playSound": true - }, - { - "callsign": "ZL7IO", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:17:30.7153757+02:00", - "spotCount": 0, - "playSound": true - }, - { - "callsign": "V85NPV", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:15.8781583+02:00", - "spotCount": 0, - "playSound": true - }, - { - "callsign": "YI1MB", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:18.825584+02:00", - "spotCount": 0, - "playSound": true - }, - { - "callsign": "C5R", - "notes": "", - "lastSeen": "2025-10-18T18:05:16.1881069+02:00", - "lastSeenStr": "Just now", - "addedAt": "2025-10-18T17:18:04.5006892+02:00", - "spotCount": 5, - "playSound": true - }, - { - "callsign": "9L8MD", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:56.7896868+02:00", - "spotCount": 0, - "playSound": true - }, { "callsign": "5K0UA", "notes": "", - "lastSeen": "2025-10-18T18:48:03.346559+02:00", - "lastSeenStr": "Just now", + "lastSeen": "2025-10-19T10:05:21.8975474+02:00", + "lastSeenStr": "5 minutes ago", "addedAt": "2025-10-18T17:17:53.7390559+02:00", - "spotCount": 10, - "playSound": true - }, - { - "callsign": "TZ4AM", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:19:00.3154177+02:00", - "spotCount": 0, - "playSound": true - }, - { - "callsign": "9L9L", - "notes": "", - "lastSeen": "0001-01-01T00:00:00Z", - "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:53.3401773+02:00", - "spotCount": 0, + "spotCount": 45, "playSound": true }, { @@ -234,21 +243,21 @@ "playSound": true }, { - "callsign": "VP8LP", + "callsign": "PY0FB", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:49.0576187+02:00", + "addedAt": "2025-10-18T17:17:24.3843986+02:00", "spotCount": 0, "playSound": true }, { - "callsign": "EL2BG", + "callsign": "C5R", "notes": "", - "lastSeen": "2025-10-18T18:32:28.3424341+02:00", + "lastSeen": "2025-10-19T10:11:33.5959073+02:00", "lastSeenStr": "Just now", - "addedAt": "2025-10-18T17:18:10.2000017+02:00", - "spotCount": 2, + "addedAt": "2025-10-18T17:18:04.5006892+02:00", + "spotCount": 57, "playSound": true }, { @@ -261,29 +270,20 @@ "playSound": true }, { - "callsign": "E6AD", - "notes": "", - "lastSeen": "2025-10-18T18:43:14.6994075+02:00", - "lastSeenStr": "Just now", - "addedAt": "2025-10-18T17:17:40.8765179+02:00", - "spotCount": 25, - "playSound": true - }, - { - "callsign": "5J0EA", + "callsign": "5H3MB", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:17:51.0758741+02:00", + "addedAt": "2025-10-18T17:18:42.8402097+02:00", "spotCount": 0, "playSound": true }, { - "callsign": "XV9", + "callsign": "9L9L", "notes": "", "lastSeen": "0001-01-01T00:00:00Z", "lastSeenStr": "Never", - "addedAt": "2025-10-18T17:18:24.9155327+02:00", + "addedAt": "2025-10-18T17:18:53.3401773+02:00", "spotCount": 0, "playSound": true }