diff --git a/database.go b/database.go index 50abfda..2f6bae6 100644 --- a/database.go +++ b/database.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "strconv" + "strings" "sync" "time" @@ -361,12 +362,71 @@ func (r *Log4OMContactsRepository) GetDXCCCount() int { return count } +// Nouvelle méthode optimisée - remplacer HasWorkedCallsignBandMode par celle-ci +func (r *Log4OMContactsRepository) GetWorkedCallsignsBandMode(callsigns []string, band string, mode string) map[string]bool { + if len(callsigns) == 0 { + return make(map[string]bool) + } + + result := make(map[string]bool) + + // Construire les placeholders pour la requête IN + placeholders := make([]string, len(callsigns)) + args := make([]interface{}, 0, len(callsigns)+2) + + for i, callsign := range callsigns { + placeholders[i] = "?" + args = append(args, callsign) + } + + args = append(args, band) + + var query string + + // Gérer les cas SSB/USB/LSB + if mode == "USB" || mode == "LSB" || mode == "SSB" { + query = fmt.Sprintf( + "SELECT DISTINCT callsign FROM log WHERE callsign IN (%s) AND band = ? AND (mode = 'USB' OR mode = 'LSB' OR mode = 'SSB')", + strings.Join(placeholders, ","), + ) + } else { + query = fmt.Sprintf( + "SELECT DISTINCT callsign FROM log WHERE callsign IN (%s) AND band = ? AND mode = ?", + strings.Join(placeholders, ","), + ) + args = append(args, mode) + } + + rows, err := r.db.Query(query, args...) + if err != nil { + log.Error("could not check worked band/mode status:", err) + return result + } + defer rows.Close() + + for rows.Next() { + var callsign string + if err := rows.Scan(&callsign); err != nil { + log.Error("error scanning callsign:", err) + continue + } + result[callsign] = true + } + + return result +} + +// Garder aussi l'ancienne méthode pour compatibilité (optionnel) +func (r *Log4OMContactsRepository) HasWorkedCallsignBandMode(callsign, band, mode string) bool { + result := r.GetWorkedCallsignsBandMode([]string{callsign}, band, mode) + return result[callsign] +} + // // Flex from now on // func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot { - r.Log.Infof("GetAllSpots a été appelée avec une limite de: '%s'", limit) Spots := []FlexSpot{} @@ -378,8 +438,6 @@ func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot { query = fmt.Sprintf("SELECT * from spots ORDER BY id DESC LIMIT %s", limit) } - r.Log.Infof("Exécution de la requête SQL: %s", query) - rows, err := r.db.Query(query) if err != nil { @@ -394,8 +452,6 @@ func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot { 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 { - r.Log.Errorf("Erreur lors du scan d'une ligne de la base de données: %v", err) - return nil // Arrête le traitement s'il y a une erreur sur une ligne } diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..19c567f --- /dev/null +++ b/embed.go @@ -0,0 +1,8 @@ +package main + +import ( + _ "embed" +) + +//go:embed static/index.html +var indexHTML []byte diff --git a/httpserver.go b/httpserver.go index 35240d0..acfd731 100644 --- a/httpserver.go +++ b/httpserver.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "os" + "strings" "sync" "time" @@ -68,6 +69,21 @@ type SendCallsignRequest struct { Callsign string `json:"callsign"` } +type WatchlistSpot struct { + DX string `json:"dx"` + FrequencyMhz string `json:"frequencyMhz"` + Band string `json:"band"` + Mode string `json:"mode"` + SpotterCallsign string `json:"spotterCallsign"` + UTCTime string `json:"utcTime"` + CountryName string `json:"countryName"` + NewDXCC bool `json:"newDXCC"` + NewBand bool `json:"newBand"` + NewMode bool `json:"newMode"` + Worked bool `json:"worked"` + WorkedBandMode bool `json:"workedBandMode"` +} + var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true // Allow all origins in development @@ -120,11 +136,20 @@ func (s *HTTPServer) setupRoutes() { api.HandleFunc("/log/recent", s.getRecentQSOs).Methods("GET", "OPTIONS") api.HandleFunc("/log/stats", s.getLogStats).Methods("GET", "OPTIONS") api.HandleFunc("/log/dxcc-progress", s.getDXCCProgress).Methods("GET", "OPTIONS") + api.HandleFunc("/watchlist/spots", s.getWatchlistSpotsWithStatus).Methods("GET", "OPTIONS") + // WebSocket endpoint api.HandleFunc("/ws", s.handleWebSocket).Methods("GET") // Serve static files (dashboard) - s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static"))) + // s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static"))) + s.Router.HandleFunc("/", s.serveIndex).Methods("GET") + +} + +func (s *HTTPServer) serveIndex(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(indexHTML) } func (s *HTTPServer) handleWebSocket(w http.ResponseWriter, r *http.Request) { @@ -189,7 +214,7 @@ func (s *HTTPServer) sendInitialData(conn *websocket.Conn) { conn.WriteJSON(WSMessage{Type: "watchlist", Data: watchlist}) // Send initial log data - qsos := s.ContactRepo.GetRecentQSOs("5") + qsos := s.ContactRepo.GetRecentQSOs("10") conn.WriteJSON(WSMessage{Type: "log", Data: qsos}) logStats := s.ContactRepo.GetQSOStats() @@ -262,7 +287,7 @@ func (s *HTTPServer) broadcastUpdates() { } // Broadcast log data every 10 seconds - qsos := s.ContactRepo.GetRecentQSOs("5") + qsos := s.ContactRepo.GetRecentQSOs("10") s.broadcast <- WSMessage{Type: "log", Data: qsos} stats := s.ContactRepo.GetQSOStats() @@ -512,6 +537,81 @@ func (s *HTTPServer) removeFromWatchlist(w http.ResponseWriter, r *http.Request) s.sendJSON(w, APIResponse{Success: true, Message: "Callsign removed from watchlist"}) } +func (s *HTTPServer) getWatchlistSpotsWithStatus(w http.ResponseWriter, r *http.Request) { + // Récupérer tous les spots + allSpots := s.FlexRepo.GetAllSpots("0") + + // Récupérer la watchlist + watchlistCallsigns := s.Watchlist.GetAll() + + // Filtrer les spots de la watchlist + var relevantSpots []FlexSpot + + for _, spot := range allSpots { + isInWatchlist := false + + for _, pattern := range watchlistCallsigns { + if spot.DX == pattern || strings.HasPrefix(spot.DX, pattern) { + isInWatchlist = true + break + } + } + + if isInWatchlist { + relevantSpots = append(relevantSpots, spot) + } + } + + type BandModeKey struct { + Band string + Mode string + } + + spotsByBandMode := make(map[BandModeKey][]FlexSpot) + + for _, spot := range relevantSpots { + key := BandModeKey{Band: spot.Band, Mode: spot.Mode} + spotsByBandMode[key] = append(spotsByBandMode[key], spot) + } + + var watchlistSpots []WatchlistSpot + + for key, spots := range spotsByBandMode { + // Extraire les callsigns uniques + callsignSet := make(map[string]bool) + for _, spot := range spots { + callsignSet[spot.DX] = true + } + + callsigns := make([]string, 0, len(callsignSet)) + for callsign := range callsignSet { + callsigns = append(callsigns, callsign) + } + + workedMap := s.ContactRepo.GetWorkedCallsignsBandMode(callsigns, key.Band, key.Mode) + + // Construire les résultats + for _, spot := range spots { + watchlistSpots = append(watchlistSpots, WatchlistSpot{ + DX: spot.DX, + FrequencyMhz: spot.FrequencyMhz, + Band: spot.Band, + Mode: spot.Mode, + SpotterCallsign: spot.SpotterCallsign, + UTCTime: spot.UTCTime, + CountryName: spot.CountryName, + NewDXCC: spot.NewDXCC, + NewBand: spot.NewBand, + NewMode: spot.NewMode, + Worked: spot.Worked, + WorkedBandMode: workedMap[spot.DX], + }) + } + } + + s.sendJSON(w, APIResponse{Success: true, Data: watchlistSpots}) +} + func (s *HTTPServer) HandleSolarData(w http.ResponseWriter, r *http.Request) { // Récupérer les données depuis hamqsl.com resp, err := http.Get("https://www.hamqsl.com/solarxml.php") diff --git a/static/index.html b/static/index.html index 8659d6a..a29194a 100644 --- a/static/index.html +++ b/static/index.html @@ -773,7 +773,7 @@ const data = await response.json(); if (data.success) { - showToast(`${callsign} envoyé - Radio tunée sur ${frequency} en ${mode}`, 'success'); + showToast(`${callsign} Sent - Radio tuned on ${frequency} in ${mode}`, 'success'); } else { showToast('Échec de l\'envoi', 'error'); } @@ -814,7 +814,7 @@ async function fetchLogData() { try { // Fetch recent QSOs - const qsosResponse = await fetch(`${API_BASE_URL}/log/recent?limit=5`); + const qsosResponse = await fetch(`${API_BASE_URL}/log/recent?limit=10`); const qsosJson = await qsosResponse.json(); if (qsosJson.success) { state.recentQSOs = qsosJson.data || []; @@ -843,6 +843,21 @@ } } + async function fetchWatchlistSpotsWithStatus() { + try { + const response = await fetch(`${API_BASE_URL}/watchlist/spots`); + const json = await response.json(); + + if (json.success) { + return json.data || []; + } + return []; + } catch (error) { + console.error('Error fetching watchlist spots:', error); + return []; + } + } + function updateSolarData() { const sfiEl = document.querySelector('[data-solar="sfi"]'); if (sfiEl) { @@ -1824,7 +1839,7 @@ `; } - function updateWatchlistItems() { + async function updateWatchlistItems() { const container = document.getElementById('watchlist-items-container'); if (!container) return; @@ -1842,19 +1857,96 @@ return; } + // Récupérer les spots enrichis avec le statut "worked" + const watchlistSpots = await fetchWatchlistSpotsWithStatus(); + + // Grouper les spots par callsign/prefix + const spotsByCallsign = {}; + watchlistSpots.forEach(spot => { + // Trouver le pattern correspondant + let matchedPattern = ''; + for (const pattern of state.watchlist) { + if (spot.dx === pattern || spot.dx.startsWith(pattern)) { + matchedPattern = pattern; + break; + } + } + + if (!spotsByCallsign[matchedPattern]) { + spotsByCallsign[matchedPattern] = []; + } + spotsByCallsign[matchedPattern].push(spot); + }); + container.innerHTML = state.watchlist.map(callsign => { + const spots = spotsByCallsign[callsign] || []; + const matchingCount = spots.length; + + // Trier les spots : Needed en premier, puis Worked + spots.sort((a, b) => { + if (!a.workedBandMode && b.workedBandMode) return -1; + if (a.workedBandMode && !b.workedBandMode) return 1; + return 0; + }); + + // Afficher les spots actifs pour ce callsign + const spotsHtml = spots.length > 0 ? ` +
+ ${spots.map(spot => { + const workedIcon = spot.workedBandMode + ? '' + : ''; + + const statusBadge = spot.workedBandMode + ? 'Worked' + : 'Needed!'; + + return ` +
+
+ ${workedIcon} + ${spot.dx} + ${spot.band} + ${spot.mode} + ${spot.frequencyMhz} +
+
+ ${statusBadge} + ${spot.utcTime} +
+
+ `; + }).join('')} +
+ ` : '
No active spots
'; + + const neededCount = spots.filter(s => !s.workedBandMode).length; + let neededBadge = ''; + if (matchingCount > 0) { + neededBadge = neededCount > 0 + ? `${neededCount} needed` + : 'All worked'; + } + return ` -
-
+
+
-
${callsign}
+
+
${callsign}
+ ${matchingCount} active spot${matchingCount !== 1 ? 's' : ''} + ${neededBadge} +
+ ${spotsHtml}
`; }).join(''); diff --git a/xml.go b/xml.go index 8033c11..ddb1137 100644 --- a/xml.go +++ b/xml.go @@ -1,14 +1,16 @@ package main import ( + _ "embed" "encoding/xml" - "io" - "os" "regexp" "strings" "unicode/utf8" ) +//go:embed country.xml +var countryXMLData []byte + type Countries struct { XMLName xml.Name `xml:"Countries"` Countries []Country `xml:"Country"` @@ -56,22 +58,14 @@ type DXCC struct { } func LoadCountryFile() Countries { - // Open our xmlFile - xmlFile, err := os.Open("country.xml") - // if we os.Open returns an error then handle it - if err != nil { - os.Exit(1) - } - - // defer the closing of our xmlFile so that we can parse it later on - defer xmlFile.Close() - - // read our opened xmlFile as a byte array. - byteValue, _ := io.ReadAll(xmlFile) - var countries Countries - xml.Unmarshal(byteValue, &countries) + // Utiliser les données embarquées + err := xml.Unmarshal(countryXMLData, &countries) + if err != nil { + Log.Fatalf("Failed to parse embedded country.xml: %v", err) + } + return countries } @@ -81,19 +75,23 @@ func GetDXCC(dxCall string, Countries Countries) DXCC { // Get all the matching DXCC for current callsign for i := 0; i < len(Countries.Countries); i++ { - regExp := regexp.MustCompile(Countries.Countries[i].CountryPrefixList.CountryPrefixList[len(Countries.Countries[i].CountryPrefixList.CountryPrefixList)-1].PrefixList) + if len(Countries.Countries[i].CountryPrefixList.CountryPrefixList) == 0 { + continue + } + + lastPrefix := Countries.Countries[i].CountryPrefixList.CountryPrefixList[len(Countries.Countries[i].CountryPrefixList.CountryPrefixList)-1] + regExp := regexp.MustCompile(lastPrefix.PrefixList) match := regExp.FindStringSubmatch(dxCall) if len(match) != 0 { - d = DXCC{ Callsign: dxCall, CountryName: Countries.Countries[i].CountryName, DXCC: Countries.Countries[i].Dxcc, - RegEx: Countries.Countries[i].CountryPrefixList.CountryPrefixList[len(Countries.Countries[i].CountryPrefixList.CountryPrefixList)-1].PrefixList, + RegEx: lastPrefix.PrefixList, } - if Countries.Countries[i].CountryPrefixList.CountryPrefixList[len(Countries.Countries[i].CountryPrefixList.CountryPrefixList)-1].EndDate == "" { + if lastPrefix.EndDate == "" { d.Ended = false } else { d.Ended = true @@ -132,7 +130,7 @@ func GetDXCC(dxCall string, Countries Countries) DXCC { return DXCCMatch } else { - Log.Errorf("Could not find %s in country list", dxCall) + Log.Warnf("Could not find %s in country list", dxCall) } return DXCC{}