up
This commit is contained in:
66
database.go
66
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
|
||||
}
|
||||
|
||||
|
||||
8
embed.go
Normal file
8
embed.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed static/index.html
|
||||
var indexHTML []byte
|
||||
106
httpserver.go
106
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")
|
||||
|
||||
@@ -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
40
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{}
|
||||
|
||||
Reference in New Issue
Block a user