582 lines
34 KiB
HTML
582 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>FlexDXCluster Dashboard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
body, html {
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
}
|
|
.animate-spin { animation: spin 1s linear infinite; }
|
|
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
|
|
.full-height { height: 100vh; display: flex; flex-direction: column; }
|
|
.scrollable { overflow-y: auto; flex: 1; }
|
|
::-webkit-scrollbar { width: 8px; }
|
|
::-webkit-scrollbar-track { background: rgb(15 23 42); }
|
|
::-webkit-scrollbar-thumb { background: rgb(51 65 85); border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: rgb(71 85 105); }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
|
|
<div id="app" class="full-height p-4"></div>
|
|
|
|
<script>
|
|
const API_BASE_URL = 'http://localhost:8080/api';
|
|
|
|
let state = {
|
|
spots: [],
|
|
filteredSpots: [],
|
|
stats: {
|
|
totalSpots: 0,
|
|
activeSpotters: 0,
|
|
newDXCC: 0,
|
|
connectedClients: 0,
|
|
totalContacts: 0,
|
|
clusterStatus: 'disconnected',
|
|
flexStatus: 'disconnected',
|
|
filters: { skimmer: false, ft8: false, ft4: false }
|
|
},
|
|
topSpotters: [],
|
|
loading: true,
|
|
command: '',
|
|
spotFilters: {
|
|
showAll: true,
|
|
showNewDXCC: false,
|
|
showNewBand: false,
|
|
showNewMode: false,
|
|
showNewBandMode: false,
|
|
showNewSlot: false,
|
|
showWorked: false,
|
|
band160M: false,
|
|
band80M: false,
|
|
band60M: false,
|
|
band40M: false,
|
|
band30M: false,
|
|
band20M: false,
|
|
band17M: false,
|
|
band15M: false,
|
|
band12M: false,
|
|
band10M: false,
|
|
band6M: false
|
|
}
|
|
};
|
|
|
|
async function fetchData() {
|
|
try {
|
|
const [statsRes, spotsRes, spottersRes] = await Promise.all([
|
|
fetch(`${API_BASE_URL}/stats`),
|
|
fetch(`${API_BASE_URL}/spots?limit=100`),
|
|
fetch(`${API_BASE_URL}/spotters`)
|
|
]);
|
|
|
|
const statsData = await statsRes.json();
|
|
const spotsData = await spotsRes.json();
|
|
const spottersData = await spottersRes.json();
|
|
|
|
if (statsData.success) state.stats = statsData.data;
|
|
if (spotsData.success) {
|
|
state.spots = spotsData.data || [];
|
|
applySpotFilters();
|
|
}
|
|
if (spottersData.success) state.topSpotters = spottersData.data || [];
|
|
|
|
state.loading = false;
|
|
render();
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
state.loading = false;
|
|
render();
|
|
}
|
|
}
|
|
|
|
function applySpotFilters() {
|
|
if (state.spotFilters.showAll) {
|
|
state.filteredSpots = state.spots;
|
|
return;
|
|
}
|
|
|
|
const bandFiltersActive = state.spotFilters.band160M || state.spotFilters.band80M ||
|
|
state.spotFilters.band60M || state.spotFilters.band40M || state.spotFilters.band30M ||
|
|
state.spotFilters.band20M || state.spotFilters.band17M || state.spotFilters.band15M ||
|
|
state.spotFilters.band12M || state.spotFilters.band10M || state.spotFilters.band6M;
|
|
|
|
const typeFiltersActive = state.spotFilters.showNewDXCC || state.spotFilters.showNewBand ||
|
|
state.spotFilters.showNewMode || state.spotFilters.showNewBandMode ||
|
|
state.spotFilters.showNewSlot || state.spotFilters.showWorked;
|
|
|
|
state.filteredSpots = state.spots.filter(spot => {
|
|
let matchesBand = false;
|
|
let matchesType = false;
|
|
|
|
if (bandFiltersActive) {
|
|
matchesBand = (
|
|
(state.spotFilters.band160M && spot.Band === '160M') ||
|
|
(state.spotFilters.band80M && spot.Band === '80M') ||
|
|
(state.spotFilters.band60M && spot.Band === '60M') ||
|
|
(state.spotFilters.band40M && spot.Band === '40M') ||
|
|
(state.spotFilters.band30M && spot.Band === '30M') ||
|
|
(state.spotFilters.band20M && spot.Band === '20M') ||
|
|
(state.spotFilters.band17M && spot.Band === '17M') ||
|
|
(state.spotFilters.band15M && spot.Band === '15M') ||
|
|
(state.spotFilters.band12M && spot.Band === '12M') ||
|
|
(state.spotFilters.band10M && spot.Band === '10M') ||
|
|
(state.spotFilters.band6M && spot.Band === '6M')
|
|
);
|
|
}
|
|
|
|
if (typeFiltersActive) {
|
|
if (state.spotFilters.showNewDXCC) {
|
|
const isNewDXCC = !!spot.NewDXCC;
|
|
const isNewBand = !!spot.NewBand;
|
|
const isNewMode = !!spot.NewMode;
|
|
if (isNewDXCC && !(isNewBand && isNewMode)) {
|
|
matchesType = true;
|
|
}
|
|
}
|
|
|
|
if (state.spotFilters.showNewBandMode && !!spot.NewBand && !!spot.NewMode) matchesType = true;
|
|
if (state.spotFilters.showNewBand && !!spot.NewBand && !spot.NewMode) matchesType = true;
|
|
if (state.spotFilters.showNewMode && !!spot.NewMode && !spot.NewBand) matchesType = true;
|
|
if (state.spotFilters.showNewSlot && !!spot.NewSlot && !spot.NewDXCC && !spot.NewBand && !spot.NewMode) matchesType = true;
|
|
if (state.spotFilters.showWorked && !!spot.Worked) matchesType = true;
|
|
}
|
|
|
|
if (bandFiltersActive && !typeFiltersActive) {
|
|
return matchesBand;
|
|
} else if (typeFiltersActive && !bandFiltersActive) {
|
|
return matchesType;
|
|
} else if (bandFiltersActive && typeFiltersActive) {
|
|
return matchesBand && matchesType;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function toggleSpotFilter(filterName) {
|
|
if (filterName === 'showAll') {
|
|
state.spotFilters = {
|
|
showAll: true,
|
|
showNewDXCC: false,
|
|
showNewBand: false,
|
|
showNewMode: false,
|
|
showNewBandMode: false,
|
|
showNewSlot: false,
|
|
showWorked: false,
|
|
band160M: false,
|
|
band80M: false,
|
|
band60M: false,
|
|
band40M: false,
|
|
band30M: false,
|
|
band20M: false,
|
|
band17M: false,
|
|
band15M: false,
|
|
band12M: false,
|
|
band10M: false,
|
|
band6M: false
|
|
};
|
|
} else {
|
|
state.spotFilters.showAll = false;
|
|
state.spotFilters[filterName] = !state.spotFilters[filterName];
|
|
|
|
const anyActive = Object.keys(state.spotFilters).some(key =>
|
|
key !== 'showAll' && state.spotFilters[key]
|
|
);
|
|
if (!anyActive) {
|
|
state.spotFilters.showAll = true;
|
|
}
|
|
}
|
|
|
|
applySpotFilters();
|
|
render();
|
|
}
|
|
|
|
async function sendCommand() {
|
|
const command = state.command.trim();
|
|
if (!command) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/send-command`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
state.command = '';
|
|
alert('Command sent successfully!');
|
|
render();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending command:', error);
|
|
alert('Failed to send command');
|
|
}
|
|
}
|
|
|
|
async function updateFilter(filterName, value) {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/filters`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ [filterName]: value })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
state.stats.filters[filterName] = value;
|
|
fetchData();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating filters:', error);
|
|
}
|
|
}
|
|
|
|
async function shutdownApp() {
|
|
// if (!confirm('Are you sure you want to shutdown FlexDXCluster?')) {
|
|
// return;
|
|
// }
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/shutdown`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
// alert('FlexDXCluster is shutting down...');
|
|
// Redirect to a shutdown page or close
|
|
document.body.innerHTML = '<div class="min-h-screen flex items-center justify-center bg-slate-900 text-white"><div class="text-center"><h1 class="text-4xl font-bold mb-4">FlexDXCluster Stopped</h1><p class="text-slate-400">The application has been shut down successfully.</p></div></div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error shutting down:', error);
|
|
alert('Failed to shutdown application');
|
|
}
|
|
}
|
|
|
|
function getPriorityColor(spot) {
|
|
if (spot.NewDXCC) return 'bg-green-500/20 text-green-400 border-green-500/50';
|
|
if (spot.NewBand && spot.NewMode) return 'bg-purple-500/20 text-purple-400 border-purple-500/50';
|
|
if (spot.NewBand) return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
|
if (spot.NewMode) return 'bg-orange-500/20 text-orange-400 border-orange-500/50';
|
|
if (spot.Worked) return 'bg-cyan-500/20 text-cyan-400 border-cyan-500/50';
|
|
return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
|
}
|
|
|
|
function getStatusLabel(spot) {
|
|
if (spot.NewDXCC) return 'New DXCC';
|
|
if (spot.NewBand && spot.NewMode) return 'New B&M';
|
|
if (spot.NewBand) return 'New Band';
|
|
if (spot.NewMode) return 'New Mode';
|
|
if (spot.NewSlot) return 'New Slot';
|
|
if (spot.Worked) return 'Worked';
|
|
return '';
|
|
}
|
|
|
|
function countSpotsByType(type) {
|
|
switch(type) {
|
|
case 'newDXCC': return state.spots.filter(s => s.NewDXCC).length;
|
|
case 'newBand': return state.spots.filter(s => s.NewBand && !s.NewMode).length;
|
|
case 'newMode': return state.spots.filter(s => s.NewMode && !s.NewBand).length;
|
|
case 'newBandMode': return state.spots.filter(s => s.NewBand && s.NewMode).length;
|
|
case 'newSlot': return state.spots.filter(s => s.NewSlot && !s.NewDXCC && !s.NewBand && !s.NewMode).length;
|
|
case 'worked': return state.spots.filter(s => s.Worked).length;
|
|
case '160M': return state.spots.filter(s => s.Band === '160M').length;
|
|
case '80M': return state.spots.filter(s => s.Band === '80M').length;
|
|
case '60M': return state.spots.filter(s => s.Band === '60M').length;
|
|
case '40M': return state.spots.filter(s => s.Band === '40M').length;
|
|
case '30M': return state.spots.filter(s => s.Band === '30M').length;
|
|
case '20M': return state.spots.filter(s => s.Band === '20M').length;
|
|
case '17M': return state.spots.filter(s => s.Band === '17M').length;
|
|
case '15M': return state.spots.filter(s => s.Band === '15M').length;
|
|
case '12M': return state.spots.filter(s => s.Band === '12M').length;
|
|
case '10M': return state.spots.filter(s => s.Band === '10M').length;
|
|
case '6M': return state.spots.filter(s => s.Band === '6M').length;
|
|
default: return state.spots.length;
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
const app = document.getElementById('app');
|
|
|
|
if (state.loading) {
|
|
app.innerHTML = `
|
|
<div class="h-full flex items-center justify-center">
|
|
<div class="text-center">
|
|
<div class="w-12 h-12 border-4 border-blue-400 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
|
<p class="text-slate-400">Loading dashboard...</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
app.innerHTML = `
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
|
</svg>
|
|
<div>
|
|
<h1 class="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">FlexDXCluster</h1>
|
|
<p class="text-xs text-slate-400">F4BPO • ${state.stats.totalContacts} Contacts</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center gap-2 text-xs">
|
|
<div class="w-2 h-2 rounded-full ${state.stats.clusterStatus === 'connected' ? 'bg-green-500 animate-pulse' : 'bg-red-500'}"></div>
|
|
<span class="text-slate-400">Cluster</span>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-xs">
|
|
<div class="w-2 h-2 rounded-full ${state.stats.flexStatus === 'connected' ? 'bg-green-500 animate-pulse' : 'bg-red-500'}"></div>
|
|
<span class="text-slate-400">Flex</span>
|
|
</div>
|
|
<button onclick="shutdownApp()" class="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors flex items-center gap-1">
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
Shutdown
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-4 gap-3 mb-3">
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
|
<div class="flex items-center justify-between">
|
|
<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>
|
|
<p class="text-xs text-slate-400 mt-1">Total Spots</p>
|
|
</div>
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
|
<div class="flex items-center justify-between">
|
|
<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>
|
|
<p class="text-xs text-slate-400 mt-1">New DXCC</p>
|
|
</div>
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
|
<div class="flex items-center justify-between">
|
|
<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>
|
|
<p class="text-xs text-slate-400 mt-1">Spotters</p>
|
|
</div>
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
|
<div class="flex items-center justify-between">
|
|
<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>
|
|
<p class="text-xs text-slate-400 mt-1">Clients</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3 mb-3">
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
|
<div class="flex gap-2 items-center">
|
|
<input id="commandInput" type="text" value="${state.command}" placeholder="Command..."
|
|
class="flex-1 px-3 py-1.5 text-sm bg-slate-900/50 border border-slate-600 rounded focus:outline-none focus:border-blue-500 text-white" />
|
|
<button onclick="sendCommand()" class="px-4 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 rounded transition-colors">Send</button>
|
|
</div>
|
|
</div>
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
|
<div class="flex gap-3 text-sm">
|
|
<label class="flex items-center gap-1.5 cursor-pointer">
|
|
<input type="checkbox" ${state.stats.filters.skimmer ? 'checked' : ''}
|
|
onchange="updateFilter('skimmer', this.checked)" class="w-4 h-4 rounded" />
|
|
<span>Skimmer</span>
|
|
</label>
|
|
<label class="flex items-center gap-1.5 cursor-pointer">
|
|
<input type="checkbox" ${state.stats.filters.ft8 ? 'checked' : ''}
|
|
onchange="updateFilter('ft8', this.checked)" class="w-4 h-4 rounded" />
|
|
<span>FT8</span>
|
|
</label>
|
|
<label class="flex items-center gap-1.5 cursor-pointer">
|
|
<input type="checkbox" ${state.stats.filters.ft4 ? 'checked' : ''}
|
|
onchange="updateFilter('ft4', this.checked)" class="w-4 h-4 rounded" />
|
|
<span>FT4</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50 mb-3">
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<h3 class="text-xs font-bold text-slate-400 mb-2">TYPE FILTERS</h3>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button onclick="toggleSpotFilter('showAll')"
|
|
class="px-3 py-1 text-sm 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('showNewDXCC')"
|
|
class="px-3 py-1 text-sm 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-3 py-1 text-sm 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-3 py-1 text-sm 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-3 py-1 text-sm 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-3 py-1 text-sm 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-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showWorked ? 'bg-cyan-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
Worked (${countSpotsByType('worked')})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xs font-bold text-slate-400 mb-2">BAND FILTERS</h3>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button onclick="toggleSpotFilter('band160M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band160M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
160M (${countSpotsByType('160M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band80M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band80M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
80M (${countSpotsByType('80M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band60M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band60M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
60M (${countSpotsByType('60M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band40M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band40M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
40M (${countSpotsByType('40M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band30M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band30M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
30M (${countSpotsByType('30M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band20M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band20M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
20M (${countSpotsByType('20M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band17M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band17M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
17M (${countSpotsByType('17M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band15M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band15M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
15M (${countSpotsByType('15M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band12M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band12M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
12M (${countSpotsByType('12M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band10M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band10M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
10M (${countSpotsByType('10M')})
|
|
</button>
|
|
<button onclick="toggleSpotFilter('band6M')"
|
|
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band6M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
|
6M (${countSpotsByType('6M')})
|
|
</button>
|
|
</div>
|
|
</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">
|
|
<div class="p-3 border-b border-slate-700/50">
|
|
<h2 class="text-lg font-bold">Recent Spots (${state.filteredSpots.length})</h2>
|
|
</div>
|
|
<div class="scrollable">
|
|
<table class="w-full">
|
|
<thead class="bg-slate-900/50 sticky top-0">
|
|
<tr class="text-left text-xs text-slate-400">
|
|
<th class="p-2">DX</th>
|
|
<th class="p-2">Freq</th>
|
|
<th class="p-2">Band</th>
|
|
<th class="p-2">Mode</th>
|
|
<th class="p-2">Spotter</th>
|
|
<th class="p-2">Time</th>
|
|
<th class="p-2">Country</th>
|
|
<th class="p-2">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${state.filteredSpots.map(spot => `
|
|
<tr class="border-b border-slate-700/30 hover:bg-slate-700/30 transition-colors text-sm">
|
|
<td class="p-2 font-bold text-blue-400">${spot.DX}</td>
|
|
<td class="p-2 font-mono text-xs">${spot.FrequencyMhz}</td>
|
|
<td class="p-2"><span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">${spot.Band}</span></td>
|
|
<td class="p-2"><span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded text-xs">${spot.Mode}</span></td>
|
|
<td class="p-2 text-slate-300 text-xs">${spot.SpotterCallsign}</td>
|
|
<td class="p-2 text-slate-400 text-xs">${spot.UTCTime}</td>
|
|
<td class="p-2 text-xs">${spot.CountryName || 'N/A'}</td>
|
|
<td class="p-2">
|
|
${getStatusLabel(spot) ? `<span class="px-1.5 py-0.5 rounded text-xs font-semibold border ${getPriorityColor(spot)}">${getStatusLabel(spot)}</span>` : ''}
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg border border-slate-700/50 flex flex-col overflow-hidden">
|
|
<div class="p-3 border-b border-slate-700/50">
|
|
<h2 class="text-lg font-bold">Top Spotters</h2>
|
|
</div>
|
|
<div class="scrollable p-3">
|
|
${state.topSpotters.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('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const commandInput = document.getElementById('commandInput');
|
|
if (commandInput) {
|
|
commandInput.addEventListener('input', (e) => {
|
|
state.command = e.target.value;
|
|
});
|
|
commandInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') sendCommand();
|
|
});
|
|
}
|
|
}
|
|
|
|
fetchData();
|
|
setInterval(fetchData, 1000);
|
|
</script>
|
|
</body>
|
|
</html> |