This commit is contained in:
2025-10-12 15:56:19 +05:30
parent 1488c517db
commit f047796b54
5 changed files with 290 additions and 36 deletions

View File

@@ -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
}

8
embed.go Normal file
View File

@@ -0,0 +1,8 @@
package main
import (
_ "embed"
)
//go:embed static/index.html
var indexHTML []byte

View File

@@ -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")

View File

@@ -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 ? `
<div class="mt-2 space-y-1 max-h-48 overflow-y-auto">
${spots.map(spot => {
const workedIcon = spot.workedBandMode
? '<svg class="w-4 h-4 text-green-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>'
: '<svg class="w-4 h-4 text-orange-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>';
const statusBadge = spot.workedBandMode
? '<span class="px-1.5 py-0.5 bg-green-500/20 text-green-400 rounded text-xs font-semibold">Worked</span>'
: '<span class="px-1.5 py-0.5 bg-orange-500/20 text-orange-400 rounded text-xs font-semibold">Needed!</span>';
return `
<div class="flex items-center justify-between p-2 bg-slate-800/50 rounded text-xs cursor-pointer hover:bg-slate-700/50 transition-colors ${!spot.workedBandMode ? 'border-l-2 border-orange-500' : ''}"
onclick="sendCallsignToLog4OM('${spot.dx}', '${spot.frequencyMhz}', '${spot.mode}')"
title="Click to send to Log4OM and tune radio">
<div class="flex items-center gap-2 flex-1 min-w-0">
${workedIcon}
<span class="font-bold text-blue-400">${spot.dx}</span>
<span class="px-1.5 py-0.5 bg-slate-700/50 rounded flex-shrink-0">${spot.band}</span>
<span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded flex-shrink-0">${spot.mode}</span>
<span class="text-slate-400 font-mono truncate">${spot.frequencyMhz}</span>
</div>
<div class="flex items-center gap-2 flex-shrink-0 ml-2">
${statusBadge}
<span class="text-slate-500">${spot.utcTime}</span>
</div>
</div>
`;
}).join('')}
</div>
` : '<div class="mt-2 text-xs text-slate-500 text-center py-2 bg-slate-800/30 rounded">No active spots</div>';
const neededCount = spots.filter(s => !s.workedBandMode).length;
let neededBadge = '';
if (matchingCount > 0) {
neededBadge = neededCount > 0
? `<span class="px-1.5 py-0.5 bg-orange-500/20 text-orange-400 rounded text-xs font-semibold">${neededCount} needed</span>`
: '<span class="px-1.5 py-0.5 bg-green-500/20 text-green-400 rounded text-xs font-semibold">All worked</span>';
}
return `
<div class="mb-2 p-3 bg-slate-900/30 rounded hover:bg-slate-700/30 transition-colors" data-watchlist="${callsign}">
<div class="flex items-center justify-between">
<div class="mb-3 p-3 bg-slate-900/30 rounded hover:bg-slate-700/30 transition-colors border ${neededCount > 0 ? 'border-orange-500/30' : 'border-slate-700/50'}" data-watchlist="${callsign}">
<div class="flex items-center justify-between mb-2">
<div class="flex-1">
<div class="font-bold text-pink-400">${callsign}</div>
<div class="flex items-center gap-2 flex-wrap">
<div class="font-bold text-pink-400 text-lg">${callsign}</div>
<span class="text-xs text-slate-400">${matchingCount} active spot${matchingCount !== 1 ? 's' : ''}</span>
${neededBadge}
</div>
</div>
<button
onclick="removeFromWatchlist('${callsign}')"
onclick="event.stopPropagation(); removeFromWatchlist('${callsign}')"
class="px-2 py-1 text-xs bg-red-600/20 hover:bg-red-600/40 text-red-400 rounded transition-colors">
Remove
</button>
</div>
${spotsHtml}
</div>
`;
}).join('');

40
xml.go
View File

@@ -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{}