This commit is contained in:
2025-10-12 13:58:21 +05:30
parent 5ba5f29b24
commit 1488c517db
6 changed files with 530 additions and 201 deletions

View File

@@ -366,6 +366,7 @@ func (r *Log4OMContactsRepository) GetDXCCCount() int {
//
func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot {
r.Log.Infof("GetAllSpots a été appelée avec une limite de: '%s'", limit)
Spots := []FlexSpot{}
@@ -377,6 +378,8 @@ 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 {
@@ -390,8 +393,10 @@ func (r *FlexDXClusterRepository) GetAllSpots(limit string) []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 {
fmt.Println(err)
return 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
}
Spots = append(Spots, s)

View File

@@ -205,7 +205,6 @@ func (fc *FlexClient) StartFlexClient() {
case <-fc.ctx.Done():
return
case <-fc.SpotChanToFlex:
// Ignorer les spots
}
}
}()
@@ -420,7 +419,7 @@ func (fc *FlexClient) SendSpottoFlex(spot TelnetSpot) {
if fc.HTTPServer != nil {
fc.HTTPServer.broadcast <- WSMessage{
Type: "spots",
Data: fc.Repo.GetAllSpots("1000"),
Data: fc.Repo.GetAllSpots("0"),
}
}

View File

@@ -177,7 +177,7 @@ func (s *HTTPServer) sendInitialData(conn *websocket.Conn) {
conn.WriteJSON(WSMessage{Type: "stats", Data: stats})
// Send initial spots
spots := s.FlexRepo.GetAllSpots("1000")
spots := s.FlexRepo.GetAllSpots("0")
conn.WriteJSON(WSMessage{Type: "spots", Data: spots})
// Send initial spotters
@@ -224,7 +224,7 @@ func (s *HTTPServer) handleBroadcasts() {
}
func (s *HTTPServer) broadcastUpdates() {
statsTicker := time.NewTicker(500 * time.Millisecond)
statsTicker := time.NewTicker(1 * time.Second)
logTicker := time.NewTicker(10 * time.Second)
defer statsTicker.Stop()
defer logTicker.Stop()
@@ -245,7 +245,7 @@ func (s *HTTPServer) broadcastUpdates() {
s.broadcast <- WSMessage{Type: "stats", Data: stats}
// Broadcast spots
spots := s.FlexRepo.GetAllSpots("2000")
spots := s.FlexRepo.GetAllSpots("0")
s.broadcast <- WSMessage{Type: "spots", Data: spots}
// Broadcast spotters

View File

@@ -290,9 +290,6 @@ func (spot *TelnetSpot) GuessMode() {
if freqInt >= 14074 && freqInt < 14078 {
spot.Mode = "FT8"
}
if freqInt >= 14074 && freqInt < 14078 {
spot.Mode = "FT8"
}
if freqInt >= 14078 && freqInt < 14083 {
spot.Mode = "FT4"
}

View File

@@ -326,7 +326,7 @@
wsReconnectAttempts = 0;
hideErrorBanner();
showToast('Connected to server', 'success');
render();
render();
};
ws.onmessage = (event) => {
@@ -371,30 +371,333 @@
switch (message.type) {
case 'stats':
state.stats = message.data;
updateStatsOnly();
break;
case 'spots':
state.spots = message.data || [];
applySpotFilters();
updateSpotsTable();
updateSpotCounts();
if (state.activeTab === 'stats') {
updateBandPropagation();
}
break;
case 'spotters':
state.topSpotters = message.data || [];
if (state.activeTab === 'stats') {
updateTopSpotters();
}
break;
case 'watchlist':
state.watchlist = message.data || [];
// Force la mise à jour de la watchlist même si on est dans l'input
if (state.activeTab === 'watchlist') {
updateWatchlistItems();
updateWatchlistCounters();
}
}
break;
case 'log':
state.recentQSOs = message.data || [];
if (state.activeTab === 'log') {
updateLogTable();
}
break;
case 'logStats':
state.logStats = message.data || {};
if (state.activeTab === 'log') {
updateLogStats();
}
break;
}
}
// Mettre à jour uniquement les stats
function updateStatsOnly() {
// Total Spots
const totalSpotsEl = document.querySelector('[data-stat="totalSpots"]');
if (totalSpotsEl) totalSpotsEl.textContent = state.stats.totalSpots;
// New DXCC
const newDxccEl = document.querySelector('[data-stat="newDXCC"]');
if (newDxccEl) newDxccEl.textContent = state.stats.newDXCC;
// Active Spotters
const spottersEl = document.querySelector('[data-stat="activeSpotters"]');
if (spottersEl) spottersEl.textContent = state.stats.activeSpotters;
// Connected Clients
const clientsEl = document.querySelector('[data-stat="connectedClients"]');
if (clientsEl) clientsEl.textContent = state.stats.connectedClients;
const totalContactsEl = document.querySelector('[data-stat="totalContacts"]');
if (totalContactsEl) totalContactsEl.textContent = state.stats.totalContacts;
// Status indicators
updateStatusIndicators();
updateFilterToggles();
}
function updateBandPropagation() {
const container = document.querySelector('[data-section="band-propagation"]');
if (!container) return;
const bandStats = getBandStats();
const maxSpots = Math.max(...Object.values(bandStats), 1);
container.innerHTML = BANDS.map(band => {
const count = bandStats[band];
const percentage = maxSpots > 0 ? (count / maxSpots) * 100 : 0;
const color = BAND_COLORS[band];
return `
<div class="band-bar" onclick="toggleSpotFilter('band${band}')">
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-semibold" style="color: ${color}">${band}</span>
<span class="text-xs text-slate-400">${count} spots</span>
</div>
<div class="w-full bg-slate-700/30 rounded-full h-2 overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
style="width: ${percentage}%; background-color: ${color}">
</div>
</div>
</div>
`;
}).join('');
}
function updateFilterButtons() {
// Mise à jour du bouton "All"
const allBtn = document.querySelector('[data-filter="showAll"]');
if (allBtn) {
if (state.spotFilters.showAll) {
allBtn.className = 'px-2 py-0.5 text-xs rounded transition-colors bg-blue-600 text-white';
} else {
allBtn.className = 'px-2 py-0.5 text-xs rounded transition-colors bg-slate-700/50 text-slate-300 hover:bg-slate-700';
}
render();
}
// Mise à jour des filtres de type
const typeFilters = [
{ key: 'showWatchlist', color: 'pink' },
{ key: 'showNewDXCC', color: 'green' },
{ key: 'showNewBandMode', color: 'purple' },
{ key: 'showNewBand', color: 'yellow' },
{ key: 'showNewMode', color: 'orange' },
{ key: 'showNewSlot', color: 'sky' },
{ key: 'showWorked', color: 'cyan' }
];
typeFilters.forEach(filter => {
const btn = document.querySelector(`[data-filter="${filter.key}"]`);
if (btn) {
if (state.spotFilters[filter.key]) {
btn.className = `px-2 py-0.5 text-xs rounded transition-colors bg-${filter.color}-600 text-white`;
} else {
btn.className = 'px-2 py-0.5 text-xs rounded transition-colors bg-slate-700/50 text-slate-300 hover:bg-slate-700';
}
}
});
// Mise à jour des filtres de mode
const modeFilters = [
{ key: 'showDigital', color: 'teal' },
{ key: 'showSSB', color: 'amber' },
{ key: 'showCW', color: 'rose' }
];
modeFilters.forEach(filter => {
const btn = document.querySelector(`[data-filter="${filter.key}"]`);
if (btn) {
if (state.spotFilters[filter.key]) {
btn.className = `px-2 py-0.5 text-xs rounded transition-colors bg-${filter.color}-600 text-white`;
} else {
btn.className = 'px-2 py-0.5 text-xs rounded transition-colors bg-slate-700/50 text-slate-300 hover:bg-slate-700';
}
}
});
// Mise à jour des filtres de bande
const bands = ['160M', '80M', '60M', '40M', '30M', '20M', '17M', '15M', '12M', '10M', '6M'];
bands.forEach(band => {
const key = `band${band}`;
const btn = document.querySelector(`[data-filter="${key}"]`);
if (btn) {
if (state.spotFilters[key]) {
btn.className = 'px-1.5 py-0.5 text-xs rounded transition-colors bg-indigo-600 text-white';
} else {
btn.className = 'px-1.5 py-0.5 text-xs rounded transition-colors bg-slate-700/50 text-slate-300 hover:bg-slate-700';
}
}
});
}
function updateStatusIndicators() {
// Cluster status
const clusterStatus = document.querySelector('[data-status="cluster"]');
if (clusterStatus) {
const isConnected = state.stats.clusterStatus === 'connected';
clusterStatus.className = `status-indicator ${isConnected ? 'status-connected' : 'status-disconnected'}`;
clusterStatus.innerHTML = `
<span class="w-2 h-2 ${isConnected ? 'bg-green-500 animate-pulse' : 'bg-red-500'} rounded-full"></span>
Cluster
`;
}
// Flex status
const flexStatus = document.querySelector('[data-status="flex"]');
if (flexStatus) {
const isConnected = state.stats.flexStatus === 'connected';
flexStatus.className = `status-indicator ${isConnected ? 'status-connected' : 'status-disconnected'}`;
flexStatus.innerHTML = `
<span class="w-2 h-2 ${isConnected ? 'bg-green-500 animate-pulse' : 'bg-red-500'} rounded-full"></span>
Flex
`;
}
}
function updateSpotCounts() {
// Vider le cache
countCache = {};
// Mise à jour manuelle de chaque bouton par son data-filter
const filters = [
{ selector: '[data-filter="showAll"]', count: state.spots.length },
{ selector: '[data-filter="showWatchlist"]', count: countWatchlistSpots() },
{ selector: '[data-filter="showNewDXCC"]', count: countSpotsByType('newDXCC') },
{ selector: '[data-filter="showNewBandMode"]', count: countSpotsByType('newBandMode') },
{ selector: '[data-filter="showNewBand"]', count: countSpotsByType('newBand') },
{ selector: '[data-filter="showNewMode"]', count: countSpotsByType('newMode') },
{ selector: '[data-filter="showNewSlot"]', count: countSpotsByType('newSlot') },
{ selector: '[data-filter="showWorked"]', count: countSpotsByType('worked') },
{ selector: '[data-filter="showDigital"]', count: countSpotsByMode('digital') },
{ selector: '[data-filter="showSSB"]', count: countSpotsByMode('ssb') },
{ selector: '[data-filter="showCW"]', count: countSpotsByMode('cw') },
{ selector: '[data-filter="band160M"]', count: countSpotsByType('160M') },
{ selector: '[data-filter="band80M"]', count: countSpotsByType('80M') },
{ selector: '[data-filter="band60M"]', count: countSpotsByType('60M') },
{ selector: '[data-filter="band40M"]', count: countSpotsByType('40M') },
{ selector: '[data-filter="band30M"]', count: countSpotsByType('30M') },
{ selector: '[data-filter="band20M"]', count: countSpotsByType('20M') },
{ selector: '[data-filter="band17M"]', count: countSpotsByType('17M') },
{ selector: '[data-filter="band15M"]', count: countSpotsByType('15M') },
{ selector: '[data-filter="band12M"]', count: countSpotsByType('12M') },
{ selector: '[data-filter="band10M"]', count: countSpotsByType('10M') },
{ selector: '[data-filter="band6M"]', count: countSpotsByType('6M') }
];
filters.forEach(filter => {
const btn = document.querySelector(filter.selector);
if (btn) {
// Remplacer le nombre entre parenthèses
btn.textContent = btn.textContent.replace(/\(\d+\)/, `(${filter.count})`);
}
});
}
function updateTopSpotters() {
const container = document.querySelector('[data-section="top-spotters"]');
if (!container) return;
container.innerHTML = state.topSpotters.slice(0, 3).map((spotter, index) => `
<div class="flex items-center justify-between mb-2 p-2 bg-slate-900/30 rounded hover:bg-slate-700/30 transition-colors">
<div class="flex items-center gap-2">
<div class="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-xs font-bold">
${index + 1}
</div>
<span class="text-sm font-semibold">${spotter.Spotter}</span>
</div>
<span class="text-slate-400 font-mono text-xs">${spotter.NumberofSpots}</span>
</div>
`).join('');
}
function updateLogTable() {
const container = document.querySelector('[data-section="recent-qsos"]');
if (!container) return;
if (state.recentQSOs.length === 0) {
container.innerHTML = `
<div class="text-center py-8 text-slate-400">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="text-sm">No QSOs in log</p>
</div>
`;
return;
}
container.innerHTML = `
<table class="w-full text-xs">
<thead class="bg-slate-900/50 sticky top-0">
<tr class="text-left text-xs text-slate-400">
<th class="p-2 font-semibold">Date</th>
<th class="p-2 font-semibold">Time</th>
<th class="p-2 font-semibold">Callsign</th>
<th class="p-2 font-semibold">Band</th>
<th class="p-2 font-semibold">Mode</th>
<th class="p-2 font-semibold">RST S/R</th>
<th class="p-2 font-semibold">Country</th>
</tr>
</thead>
<tbody>
${state.recentQSOs.map(qso => {
let qsoDate = 'N/A';
let qsoTime = 'N/A';
if (qso.date) {
const d = new Date(qso.date.replace(' ', 'T')); // Assure la compatibilité
qsoDate = d.toISOString().split('T')[0];
qsoTime = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
}
return `
<tr class="border-b border-slate-700/30 hover:bg-slate-700/30 transition-colors">
<td class="p-2 text-slate-300">${qsoDate}</td>
<td class="p-2 text-slate-300">${qsoTime}</td>
<td class="p-2">
<span class="font-bold text-blue-400">${qso.callsign}</span>
</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">${qso.band || 'N/A'}</span>
</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded text-xs">${qso.mode || 'N/A'}</span>
</td>
<td class="p-2 font-mono text-xs text-slate-400">${qso.rstSent || '---'} / ${qso.rstRcvd || '---'}</td>
<td class="p-2 text-slate-400">${qso.country || 'N/A'}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
function updateLogStats() {
// Mettre à jour les cartes de stats
const todayEl = document.querySelector('[data-log-stat="today"]');
if (todayEl) todayEl.textContent = state.logStats.today || 0;
const weekEl = document.querySelector('[data-log-stat="thisWeek"]');
if (weekEl) weekEl.textContent = state.logStats.thisWeek || 0;
const monthEl = document.querySelector('[data-log-stat="thisMonth"]');
if (monthEl) monthEl.textContent = state.logStats.thisMonth || 0;
const totalEl = document.querySelector('[data-log-stat="total"]');
if (totalEl) totalEl.textContent = state.logStats.total || 0;
// Mettre à jour la barre DXCC
const dxccWorkedEl = document.querySelector('[data-dxcc="worked"]');
if (dxccWorkedEl) dxccWorkedEl.textContent = state.dxccProgress.worked || 0;
const dxccBarEl = document.querySelector('[data-dxcc="bar"]');
if (dxccBarEl) {
dxccBarEl.style.width = `${state.dxccProgress.percentage || 0}%`;
}
const dxccPercentEl = document.querySelector('[data-dxcc="percentage"]');
if (dxccPercentEl) {
dxccPercentEl.textContent = `${(state.dxccProgress.percentage || 0).toFixed(1)}% Complete`;
}
}
// Toast notifications
@@ -531,11 +834,40 @@
state.dxccProgress = dxccJson.data || { worked: 0, total: 340, percentage: 0 };
}
render();
if (state.activeTab === 'log') {
updateLogTable();
updateLogStats();
}
} catch (error) {
console.error('Error fetching log data:', error);
}
}
function updateSolarData() {
const sfiEl = document.querySelector('[data-solar="sfi"]');
if (sfiEl) {
sfiEl.className = getSFIColor(state.solarData.sfi);
sfiEl.textContent = state.solarData.sfi;
}
const ssnEl = document.querySelector('[data-solar="ssn"]');
if (ssnEl) {
ssnEl.className = getSunspotsColor(state.solarData.sunspots);
ssnEl.textContent = state.solarData.sunspots;
}
const aIndexEl = document.querySelector('[data-solar="aIndex"]');
if (aIndexEl) {
aIndexEl.className = getAIndexColor(state.solarData.aIndex);
aIndexEl.textContent = state.solarData.aIndex;
}
const kIndexEl = document.querySelector('[data-solar="kIndex"]');
if (kIndexEl) {
kIndexEl.className = getKIndexColor(state.solarData.kIndex);
kIndexEl.textContent = state.solarData.kIndex;
}
}
function applySpotFilters() {
if (state.spotFilters.showAll) {
@@ -682,7 +1014,9 @@
}
applySpotFilters();
render();
updateSpotsTable();
updateSpotCounts();
updateFilterButtons();
}
async function updateFilter(filterName, value) {
@@ -869,38 +1203,6 @@
return 'text-red-400';
}
function renderBandPropagationChart() {
const bandStats = getBandStats();
const maxSpots = Math.max(...Object.values(bandStats), 1);
return `
<div class="p-3 border-t border-slate-700/50 flex-shrink-0">
<h2 class="text-lg font-bold mb-3">Band Propagation</h2>
</div>
<div class="px-3 pb-3 space-y-2">
${BANDS.map(band => {
const count = bandStats[band];
const percentage = maxSpots > 0 ? (count / maxSpots) * 100 : 0;
const color = BAND_COLORS[band];
return `
<div class="band-bar" onclick="toggleSpotFilter('band${band.replace('M', 'M')}')">
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-semibold" style="color: ${color}">${band}</span>
<span class="text-xs text-slate-400">${count} spots</span>
</div>
<div class="w-full bg-slate-700/30 rounded-full h-2 overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
style="width: ${percentage}%; background-color: ${color}">
</div>
</div>
</div>
`;
}).join('')}
</div>
`;
}
async function addToWatchlist() {
const input = document.getElementById('watchlist-input');
const callsign = input.value.trim().toUpperCase();
@@ -1035,34 +1337,34 @@
<div>
<h1 class="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">FlexDXCluster</h1>
<div class="flex items-center gap-3 text-xs text-slate-400">
<span>F4BPO • ${state.stats.totalContacts} Contacts</span>
<span>F4BPO • <span data-stat="totalContacts">${state.stats.totalContacts}</span> Contacts</span>
<span class="text-slate-600">|</span>
<span class="flex items-center gap-1">
<span class="font-semibold text-amber-400">SFI:</span>
<span class="${getSFIColor(state.solarData.sfi)}">${state.solarData.sfi}</span>
<span data-solar="sfi" class="${getSFIColor(state.solarData.sfi)}">${state.solarData.sfi}</span>
</span>
<span class="flex items-center gap-1">
<span class="font-semibold text-yellow-400">SSN:</span>
<span class="${getSunspotsColor(state.solarData.sunspots)}">${state.solarData.sunspots}</span>
<span data-solar="ssn" class="${getSunspotsColor(state.solarData.sunspots)}">${state.solarData.sunspots}</span>
</span>
<span class="flex items-center gap-1">
<span class="font-semibold text-red-400">A:</span>
<span class="${getAIndexColor(state.solarData.aIndex)}">${state.solarData.aIndex}</span>
<span data-solar="aIndex" class="${getAIndexColor(state.solarData.aIndex)}">${state.solarData.aIndex}</span>
</span>
<span class="flex items-center gap-1">
<span class="font-semibold text-purple-400">K:</span>
<span class="${getKIndexColor(state.solarData.kIndex)}">${state.solarData.kIndex}</span>
<span data-solar="kIndex" class="${getKIndexColor(state.solarData.kIndex)}">${state.solarData.kIndex}</span>
</span>
</div>
</div>
</div>
<div class="flex items-center gap-3">
${getWebSocketStatusHTML()}
<span class="status-indicator ${state.stats.clusterStatus === 'connected' ? 'status-connected' : 'status-disconnected'}">
<span class="status-indicator ${state.stats.clusterStatus === 'connected' ? 'status-connected' : 'status-disconnected'}" data-status="cluster">
<span class="w-2 h-2 ${state.stats.clusterStatus === 'connected' ? 'bg-green-500' : 'bg-red-500'} rounded-full ${state.stats.clusterStatus === 'connected' ? 'animate-pulse' : ''}"></span>
Cluster
</span>
<span class="status-indicator ${state.stats.flexStatus === 'connected' ? 'status-connected' : 'status-disconnected'}">
<span class="status-indicator ${state.stats.flexStatus === 'connected' ? 'status-connected' : 'status-disconnected'}" data-status="flex">
<span class="w-2 h-2 ${state.stats.flexStatus === 'connected' ? 'bg-green-500' : 'bg-red-500'} rounded-full ${state.stats.flexStatus === 'connected' ? 'animate-pulse' : ''}"></span>
Flex
</span>
@@ -1081,7 +1383,7 @@
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<div class="text-xl font-bold text-blue-400">${state.stats.totalSpots}</div>
<div class="text-xl font-bold text-blue-400" data-stat="totalSpots">${state.stats.totalSpots}</div>
</div>
<p class="text-xs text-slate-400 mt-1">Total Spots</p>
</div>
@@ -1090,7 +1392,7 @@
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-xl font-bold text-green-400">${state.stats.newDXCC}</div>
<div class="text-xl font-bold text-green-400" data-stat="newDXCC">${state.stats.newDXCC}</div>
</div>
<p class="text-xs text-slate-400 mt-1">New DXCC</p>
</div>
@@ -1099,7 +1401,7 @@
<svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<div class="text-xl font-bold text-purple-400">${state.stats.activeSpotters}</div>
<div class="text-xl font-bold text-purple-400" data-stat="activeSpotters">${state.stats.activeSpotters}</div>
</div>
<p class="text-xs text-slate-400 mt-1">Spotters</p>
</div>
@@ -1108,7 +1410,7 @@
<svg class="w-6 h-6 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
<div class="text-xl font-bold text-orange-400">${state.stats.connectedClients}</div>
<div class="text-xl font-bold text-orange-400" data-stat="connectedClients">${state.stats.connectedClients}</div>
</div>
<p class="text-xs text-slate-400 mt-1">Clients</p>
</div>
@@ -1118,7 +1420,7 @@
onchange="updateFilter('skimmer', this.checked)" />
<label for="skimmer" class="toggle-label">
<div class="toggle-switch"></div>
<span class="text-sm font-medium">Skimmer</span>
<span class="text-sm font-medium">CW</span>
</label>
<input type="checkbox" id="ft8" class="toggle-checkbox" ${state.stats.filters.ft8 ? 'checked' : ''}
@@ -1138,105 +1440,105 @@
</div>
</div>
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-2 border border-slate-700/50 mb-3">
<div class="flex items-center gap-1 flex-wrap">
<span class="text-xs font-bold text-slate-400 mr-2">TYPE:</span>
<button onclick="toggleSpotFilter('showAll')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showAll ? 'bg-blue-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
All (${state.spots.length})
</button>
<button onclick="toggleSpotFilter('showWatchlist')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showWatchlist ? 'bg-pink-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Watch (${countWatchlistSpots()})
</button>
<button onclick="toggleSpotFilter('showNewDXCC')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewDXCC ? 'bg-green-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
DXCC (${countSpotsByType('newDXCC')})
</button>
<button onclick="toggleSpotFilter('showNewBandMode')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewBandMode ? 'bg-purple-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
B&M (${countSpotsByType('newBandMode')})
</button>
<button onclick="toggleSpotFilter('showNewBand')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewBand ? 'bg-yellow-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Band (${countSpotsByType('newBand')})
</button>
<button onclick="toggleSpotFilter('showNewMode')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewMode ? 'bg-orange-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Mode (${countSpotsByType('newMode')})
</button>
<button onclick="toggleSpotFilter('showNewSlot')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewSlot ? 'bg-sky-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Slot (${countSpotsByType('newSlot')})
</button>
<button onclick="toggleSpotFilter('showWorked')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showWorked ? 'bg-cyan-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Wkd (${countSpotsByType('worked')})
</button>
<span class="text-slate-600 mx-1">|</span>
<span class="text-xs font-bold text-slate-400 mr-2">MODE:</span>
<button onclick="toggleSpotFilter('showDigital')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showDigital ? 'bg-teal-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Digi (${countSpotsByMode('digital')})
</button>
<button onclick="toggleSpotFilter('showSSB')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showSSB ? 'bg-amber-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
SSB (${countSpotsByMode('ssb')})
</button>
<button onclick="toggleSpotFilter('showCW')"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showCW ? 'bg-rose-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
CW (${countSpotsByMode('cw')})
</button>
<span class="text-slate-600 mx-1">|</span>
<span class="text-xs font-bold text-slate-400 mr-2">BAND:</span>
<button onclick="toggleSpotFilter('band160M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band160M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
160 (${countSpotsByType('160M')})
</button>
<button onclick="toggleSpotFilter('band80M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band80M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
80 (${countSpotsByType('80M')})
</button>
<button onclick="toggleSpotFilter('band60M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band60M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
60 (${countSpotsByType('60M')})
</button>
<button onclick="toggleSpotFilter('band40M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band40M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
40 (${countSpotsByType('40M')})
</button>
<button onclick="toggleSpotFilter('band30M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band30M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
30 (${countSpotsByType('30M')})
</button>
<button onclick="toggleSpotFilter('band20M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band20M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
20 (${countSpotsByType('20M')})
</button>
<button onclick="toggleSpotFilter('band17M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band17M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
17 (${countSpotsByType('17M')})
</button>
<button onclick="toggleSpotFilter('band15M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band15M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
15 (${countSpotsByType('15M')})
</button>
<button onclick="toggleSpotFilter('band12M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band12M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
12 (${countSpotsByType('12M')})
</button>
<button onclick="toggleSpotFilter('band10M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band10M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
10 (${countSpotsByType('10M')})
</button>
<button onclick="toggleSpotFilter('band6M')"
class="px-1.5 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band6M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
6 (${countSpotsByType('6M')})
</button>
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-2 border border-slate-700/50 mb-3">
<div class="flex items-center gap-1 flex-wrap">
<span class="text-xs font-bold text-slate-400 mr-2">TYPE:</span>
<button onclick="toggleSpotFilter('showAll')" data-filter="showAll"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showAll ? 'bg-blue-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
All (${state.spots.length})
</button>
<button onclick="toggleSpotFilter('showWatchlist')" data-filter="showWatchlist"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showWatchlist ? 'bg-pink-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Watch (${countWatchlistSpots()})
</button>
<button onclick="toggleSpotFilter('showNewDXCC')" data-filter="showNewDXCC"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewDXCC ? 'bg-green-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
DXCC (${countSpotsByType('newDXCC')})
</button>
<button onclick="toggleSpotFilter('showNewBandMode')" data-filter="showNewBandMode"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewBandMode ? 'bg-purple-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
B&M (${countSpotsByType('newBandMode')})
</button>
<button onclick="toggleSpotFilter('showNewBand')" data-filter="showNewBand"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewBand ? 'bg-yellow-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Band (${countSpotsByType('newBand')})
</button>
<button onclick="toggleSpotFilter('showNewMode')" data-filter="showNewMode"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewMode ? 'bg-orange-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Mode (${countSpotsByType('newMode')})
</button>
<button onclick="toggleSpotFilter('showNewSlot')" data-filter="showNewSlot"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showNewSlot ? 'bg-sky-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Slot (${countSpotsByType('newSlot')})
</button>
<button onclick="toggleSpotFilter('showWorked')" data-filter="showWorked"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showWorked ? 'bg-cyan-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Wkd (${countSpotsByType('worked')})
</button>
<span class="text-slate-600 mx-1">|</span>
<span class="text-xs font-bold text-slate-400 mr-2">MODE:</span>
<button onclick="toggleSpotFilter('showDigital')" data-filter="showDigital"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showDigital ? 'bg-teal-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
Digi (${countSpotsByMode('digital')})
</button>
<button onclick="toggleSpotFilter('showSSB')" data-filter="showSSB"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showSSB ? 'bg-amber-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
SSB (${countSpotsByMode('ssb')})
</button>
<button onclick="toggleSpotFilter('showCW')" data-filter="showCW"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.showCW ? 'bg-rose-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
CW (${countSpotsByMode('cw')})
</button>
<span class="text-slate-600 mx-1">|</span>
<span class="text-xs font-bold text-slate-400 mr-2">BAND:</span>
<button onclick="toggleSpotFilter('band160M')" data-filter="band160M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band160M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
160 (${countSpotsByType('160M')})
</button>
<button onclick="toggleSpotFilter('band80M')" data-filter="band80M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band80M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
80 (${countSpotsByType('80M')})
</button>
<button onclick="toggleSpotFilter('band60M')" data-filter="band60M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band60M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
60 (${countSpotsByType('60M')})
</button>
<button onclick="toggleSpotFilter('band40M')" data-filter="band40M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band40M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
40 (${countSpotsByType('40M')})
</button>
<button onclick="toggleSpotFilter('band30M')" data-filter="band30M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band30M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
30 (${countSpotsByType('30M')})
</button>
<button onclick="toggleSpotFilter('band20M')" data-filter="band20M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band20M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
20 (${countSpotsByType('20M')})
</button>
<button onclick="toggleSpotFilter('band17M')" data-filter="band17M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band17M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
17 (${countSpotsByType('17M')})
</button>
<button onclick="toggleSpotFilter('band15M')" data-filter="band15M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band15M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
15 (${countSpotsByType('15M')})
</button>
<button onclick="toggleSpotFilter('band12M')" data-filter="band12M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band12M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
12 (${countSpotsByType('12M')})
</button>
<button onclick="toggleSpotFilter('band10M')" data-filter="band10M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band10M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
10 (${countSpotsByType('10M')})
</button>
<button onclick="toggleSpotFilter('band6M')" data-filter="band6M"
class="px-2 py-0.5 text-xs rounded transition-colors ${state.spotFilters.band6M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
6 (${countSpotsByType('6M')})
</button>
</div>
</div>
</div>
<div class="grid grid-cols-4 gap-3" style="height: calc(100vh - 360px);">
<div class="col-span-3 bg-slate-800/50 backdrop-blur rounded-lg border border-slate-700/50 flex flex-col overflow-hidden">
@@ -1282,7 +1584,7 @@
<div class="p-3 border-b border-slate-700/50">
<h2 class="text-lg font-bold">Top 3 Spotters</h2>
</div>
<div class="p-3">
<div class="p-3" data-section="top-spotters">
${state.topSpotters.slice(0, 3).map((spotter, index) => `
<div class="flex items-center justify-between mb-2 p-2 bg-slate-900/30 rounded hover:bg-slate-700/30 transition-colors">
<div class="flex items-center gap-2">
@@ -1296,7 +1598,12 @@
`).join('')}
</div>
${renderBandPropagationChart()}
<div class="p-3 border-t border-slate-700/50 flex-shrink-0">
<h2 class="text-lg font-bold mb-3">Band Propagation</h2>
</div>
<div class="px-3 pb-3 space-y-2" data-section="band-propagation">
<!-- Le contenu sera injecté par updateBandPropagation() -->
</div>
</div>
<!-- Watchlist Tab -->
@@ -1337,7 +1644,7 @@
<div class="grid grid-cols-4 gap-2 mb-3">
<div class="bg-slate-900/50 rounded p-2">
<div class="text-xs text-slate-400">Today</div>
<div class="text-xl font-bold text-blue-400">${state.logStats.today || 0}</div>
<div class="text-xl font-bold text-blue-400" data-log-stat="today">${state.logStats.today || 0}</div>
</div>
<div class="bg-slate-900/50 rounded p-2">
<div class="text-xs text-slate-400">This Week</div>
@@ -1361,7 +1668,7 @@
</div>
<div class="w-full bg-slate-700/30 rounded-full h-3 overflow-hidden">
<div class="h-full rounded-full bg-gradient-to-r from-green-500 to-emerald-500 transition-all duration-500"
style="width: ${state.dxccProgress.percentage || 0}%">
data-dxcc="bar" style="width: ${state.dxccProgress.percentage || 0}%">
</div>
</div>
<div class="text-xs text-slate-400 text-right mt-1">${(state.dxccProgress.percentage || 0).toFixed(1)}% Complete</div>
@@ -1385,6 +1692,7 @@
<thead class="bg-slate-900/50 sticky top-0">
<tr class="text-left text-xs text-slate-400">
<th class="p-2 font-semibold">Date</th>
<th class="p-2 font-semibold">Time</th>
<th class="p-2 font-semibold">Callsign</th>
<th class="p-2 font-semibold">Band</th>
<th class="p-2 font-semibold">Mode</th>
@@ -1393,22 +1701,32 @@
</tr>
</thead>
<tbody>
${state.recentQSOs.map(qso => `
<tr class="border-b border-slate-700/30 hover:bg-slate-700/30 transition-colors">
<td class="p-2 text-slate-300">${qso.date || 'N/A'}</td>
<td class="p-2">
<span class="font-bold text-blue-400">${qso.callsign}</span>
</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">${qso.band || 'N/A'}</span>
</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded text-xs">${qso.mode || 'N/A'}</span>
</td>
<td class="p-2 font-mono text-xs text-slate-400">${qso.rstSent || '---'} / ${qso.rstRcvd || '---'}</td>
<td class="p-2 text-slate-400">${qso.country || 'N/A'}</td>
</tr>
`).join('')}
${state.recentQSOs.map(qso => {
let qsoDate = 'N/A';
let qsoTime = 'N/A';
if (qso.date) {
const d = new Date(qso.date.replace(' ', 'T')); // Assure la compatibilité
qsoDate = d.toISOString().split('T')[0];
qsoTime = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
}
return `
<tr class="border-b border-slate-700/30 hover:bg-slate-700/30 transition-colors">
<td class="p-2 text-slate-300">${qsoDate}</td>
<td class="p-2 text-slate-300">${qsoTime}</td>
<td class="p-2">
<span class="font-bold text-blue-400">${qso.callsign}</span>
</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">${qso.band || 'N/A'}</span>
</td>
<td class="p-2">
<span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded text-xs">${qso.mode || 'N/A'}</span>
</td>
<td class="p-2 font-mono text-xs text-slate-400">${qso.rstSent || '---'} / ${qso.rstRcvd || '---'}</td>
<td class="p-2 text-slate-400">${qso.country || 'N/A'}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`}
@@ -1423,6 +1741,10 @@
updateSpotsTable();
updateWatchlistItems();
if (state.activeTab === 'stats') {
updateBandPropagation();
updateTopSpotters(); // Au cas où
}
// Restaurer le scroll de la table des spots
const newSpotsContainer = document.querySelector('.scrollable');
if (newSpotsContainer && scrollTop > 0) {
@@ -1436,6 +1758,23 @@
}
}
function updateFilterToggles() {
const skimmerCheckbox = document.getElementById('skimmer');
if (skimmerCheckbox) {
skimmerCheckbox.checked = state.stats.filters.skimmer;
}
const ft8Checkbox = document.getElementById('ft8');
if (ft8Checkbox) {
ft8Checkbox.checked = state.stats.filters.ft8;
}
const ft4Checkbox = document.getElementById('ft4');
if (ft4Checkbox) {
ft4Checkbox.checked = state.stats.filters.ft4;
}
}
function updateSpotsTable() {
const container = document.getElementById('spots-table-container');
if (!container) return;
@@ -1504,16 +1843,11 @@
}
container.innerHTML = state.watchlist.map(callsign => {
const matchingSpots = state.spots.filter(s =>
s.DX === callsign || s.DX.startsWith(callsign)
).length;
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="flex-1">
<div class="font-bold text-pink-400">${callsign}</div>
<div class="text-xs text-slate-400 spot-counter">${matchingSpots} active spot${matchingSpots !== 1 ? 's' : ''}</div>
</div>
<button
onclick="removeFromWatchlist('${callsign}')"
@@ -1532,18 +1866,6 @@
if (counterElement) {
counterElement.textContent = `${countWatchlistSpots()} matching spots`;
}
// Mettre à jour les compteurs de chaque callsign
state.watchlist.forEach(callsign => {
const matchingSpots = state.spots.filter(s =>
s.DX === callsign || s.DX.startsWith(callsign)
).length;
const counterEl = document.querySelector(`[data-watchlist="${callsign}"] .spot-counter`);
if (counterEl) {
counterEl.textContent = `${matchingSpots} active spot${matchingSpots !== 1 ? 's' : ''}`;
}
});
}
async function initPropagation() {
@@ -1552,19 +1874,25 @@
// Mettre à jour toutes les 15 minutes
setInterval(async () => {
state.solarData = await fetchPropagationData();
render();
updateSolarData();
}, 15 * 60 * 1000);
}
// Initialize
initPropagation();
async function init() {
await initPropagation();
await fetchLogData();
connectWebSocket();
}
init();
setInterval(async () => {
if (state.activeTab === 'log') {
await fetchLogData();
}
}, 10000);
fetchLogData();
connectWebSocket();
}, 5000);
// Cleanup on page unload
window.addEventListener('beforeunload', () => {

View File

@@ -1 +1 @@
["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA"]
["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA","PJ6Y","5J0EA","5K0UA","VP2M"]