up
This commit is contained in:
66
database.go
66
database.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -361,12 +362,71 @@ func (r *Log4OMContactsRepository) GetDXCCCount() int {
|
|||||||
return count
|
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
|
// Flex from now on
|
||||||
//
|
//
|
||||||
|
|
||||||
func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot {
|
func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot {
|
||||||
r.Log.Infof("GetAllSpots a été appelée avec une limite de: '%s'", limit)
|
|
||||||
|
|
||||||
Spots := []FlexSpot{}
|
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)
|
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)
|
rows, err := r.db.Query(query)
|
||||||
|
|
||||||
if err != nil {
|
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,
|
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.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
|
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"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -68,6 +69,21 @@ type SendCallsignRequest struct {
|
|||||||
Callsign string `json:"callsign"`
|
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{
|
var upgrader = websocket.Upgrader{
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
return true // Allow all origins in development
|
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/recent", s.getRecentQSOs).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/log/stats", s.getLogStats).Methods("GET", "OPTIONS")
|
api.HandleFunc("/log/stats", s.getLogStats).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/log/dxcc-progress", s.getDXCCProgress).Methods("GET", "OPTIONS")
|
api.HandleFunc("/log/dxcc-progress", s.getDXCCProgress).Methods("GET", "OPTIONS")
|
||||||
|
api.HandleFunc("/watchlist/spots", s.getWatchlistSpotsWithStatus).Methods("GET", "OPTIONS")
|
||||||
|
|
||||||
// WebSocket endpoint
|
// WebSocket endpoint
|
||||||
api.HandleFunc("/ws", s.handleWebSocket).Methods("GET")
|
api.HandleFunc("/ws", s.handleWebSocket).Methods("GET")
|
||||||
|
|
||||||
// Serve static files (dashboard)
|
// 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) {
|
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})
|
conn.WriteJSON(WSMessage{Type: "watchlist", Data: watchlist})
|
||||||
|
|
||||||
// Send initial log data
|
// Send initial log data
|
||||||
qsos := s.ContactRepo.GetRecentQSOs("5")
|
qsos := s.ContactRepo.GetRecentQSOs("10")
|
||||||
conn.WriteJSON(WSMessage{Type: "log", Data: qsos})
|
conn.WriteJSON(WSMessage{Type: "log", Data: qsos})
|
||||||
|
|
||||||
logStats := s.ContactRepo.GetQSOStats()
|
logStats := s.ContactRepo.GetQSOStats()
|
||||||
@@ -262,7 +287,7 @@ func (s *HTTPServer) broadcastUpdates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast log data every 10 seconds
|
// Broadcast log data every 10 seconds
|
||||||
qsos := s.ContactRepo.GetRecentQSOs("5")
|
qsos := s.ContactRepo.GetRecentQSOs("10")
|
||||||
s.broadcast <- WSMessage{Type: "log", Data: qsos}
|
s.broadcast <- WSMessage{Type: "log", Data: qsos}
|
||||||
|
|
||||||
stats := s.ContactRepo.GetQSOStats()
|
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"})
|
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) {
|
func (s *HTTPServer) HandleSolarData(w http.ResponseWriter, r *http.Request) {
|
||||||
// Récupérer les données depuis hamqsl.com
|
// Récupérer les données depuis hamqsl.com
|
||||||
resp, err := http.Get("https://www.hamqsl.com/solarxml.php")
|
resp, err := http.Get("https://www.hamqsl.com/solarxml.php")
|
||||||
|
|||||||
@@ -773,7 +773,7 @@
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
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 {
|
} else {
|
||||||
showToast('Échec de l\'envoi', 'error');
|
showToast('Échec de l\'envoi', 'error');
|
||||||
}
|
}
|
||||||
@@ -814,7 +814,7 @@
|
|||||||
async function fetchLogData() {
|
async function fetchLogData() {
|
||||||
try {
|
try {
|
||||||
// Fetch recent QSOs
|
// 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();
|
const qsosJson = await qsosResponse.json();
|
||||||
if (qsosJson.success) {
|
if (qsosJson.success) {
|
||||||
state.recentQSOs = qsosJson.data || [];
|
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() {
|
function updateSolarData() {
|
||||||
const sfiEl = document.querySelector('[data-solar="sfi"]');
|
const sfiEl = document.querySelector('[data-solar="sfi"]');
|
||||||
if (sfiEl) {
|
if (sfiEl) {
|
||||||
@@ -1824,7 +1839,7 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWatchlistItems() {
|
async function updateWatchlistItems() {
|
||||||
const container = document.getElementById('watchlist-items-container');
|
const container = document.getElementById('watchlist-items-container');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -1842,19 +1857,96 @@
|
|||||||
return;
|
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 => {
|
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 `
|
return `
|
||||||
<div class="mb-2 p-3 bg-slate-900/30 rounded hover:bg-slate-700/30 transition-colors" data-watchlist="${callsign}">
|
<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">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="flex-1">
|
<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>
|
</div>
|
||||||
<button
|
<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">
|
class="px-2 py-1 text-xs bg-red-600/20 hover:bg-red-600/40 text-red-400 rounded transition-colors">
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
${spotsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
40
xml.go
40
xml.go
@@ -1,14 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "embed"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed country.xml
|
||||||
|
var countryXMLData []byte
|
||||||
|
|
||||||
type Countries struct {
|
type Countries struct {
|
||||||
XMLName xml.Name `xml:"Countries"`
|
XMLName xml.Name `xml:"Countries"`
|
||||||
Countries []Country `xml:"Country"`
|
Countries []Country `xml:"Country"`
|
||||||
@@ -56,22 +58,14 @@ type DXCC struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LoadCountryFile() Countries {
|
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
|
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
|
return countries
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,19 +75,23 @@ func GetDXCC(dxCall string, Countries Countries) DXCC {
|
|||||||
|
|
||||||
// Get all the matching DXCC for current callsign
|
// Get all the matching DXCC for current callsign
|
||||||
for i := 0; i < len(Countries.Countries); i++ {
|
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)
|
match := regExp.FindStringSubmatch(dxCall)
|
||||||
if len(match) != 0 {
|
if len(match) != 0 {
|
||||||
|
|
||||||
d = DXCC{
|
d = DXCC{
|
||||||
Callsign: dxCall,
|
Callsign: dxCall,
|
||||||
CountryName: Countries.Countries[i].CountryName,
|
CountryName: Countries.Countries[i].CountryName,
|
||||||
DXCC: Countries.Countries[i].Dxcc,
|
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
|
d.Ended = false
|
||||||
} else {
|
} else {
|
||||||
d.Ended = true
|
d.Ended = true
|
||||||
@@ -132,7 +130,7 @@ func GetDXCC(dxCall string, Countries Countries) DXCC {
|
|||||||
|
|
||||||
return DXCCMatch
|
return DXCCMatch
|
||||||
} else {
|
} else {
|
||||||
Log.Errorf("Could not find %s in country list", dxCall)
|
Log.Warnf("Could not find %s in country list", dxCall)
|
||||||
}
|
}
|
||||||
|
|
||||||
return DXCC{}
|
return DXCC{}
|
||||||
|
|||||||
Reference in New Issue
Block a user