Add aircraft watchlist feature to ADS-B dashboard

- Add/remove callsigns, registrations, or ICAO codes to watch
- Alert notification and sound when watched aircraft detected
- Filter view to show only watched aircraft
- Visual highlighting with cyan border and star icon
- Watchlist persisted to localStorage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-08 21:13:49 +00:00
parent 73ac74a9d6
commit 6229c25872
2 changed files with 333 additions and 2 deletions
+1
View File
@@ -418,6 +418,7 @@ body {
}
.aircraft-item {
position: relative;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(74, 158, 255, 0.15);
border-radius: 4px;
+332 -2
View File
@@ -100,7 +100,9 @@
<option value="military">Military</option>
<option value="civil">Civil</option>
<option value="emergency">Emergency</option>
<option value="watchlist">Watchlist</option>
</select>
<button class="watchlist-btn" onclick="showWatchlistModal()" title="Manage Watchlist"></button>
<select id="rangeSelect" onchange="updateRange()" title="Range rings distance">
<option value="50">50nm</option>
<option value="100">100nm</option>
@@ -164,6 +166,9 @@
let alertsEnabled = true;
let currentView = 'map'; // 'map' or 'radar'
// Watchlist - persisted to localStorage
let watchlist = JSON.parse(localStorage.getItem('adsb_watchlist') || '[]');
// Aircraft trails
let aircraftTrails = {}; // ICAO -> [{lat, lon, alt, time}, ...]
let trailLines = {}; // ICAO -> L.polyline (array of segments)
@@ -245,11 +250,16 @@
if (alertedAircraft[icao]) return;
const militaryInfo = isMilitaryAircraft(icao, ac.callsign);
const squawkInfo = checkSquawkCode(ac);
const onWatchlist = isOnWatchlist(ac);
if (squawkInfo) {
alertedAircraft[icao] = 'emergency';
playAlertSound('emergency');
showAlertBanner(`EMERGENCY: ${squawkInfo.name} - ${ac.callsign || icao}`, '#ff0000');
} else if (onWatchlist) {
alertedAircraft[icao] = 'watchlist';
playAlertSound('military'); // Use military sound for watchlist
showAlertBanner(`WATCHLIST: ${ac.callsign || ac.registration || icao} detected!`, '#00d4ff');
} else if (militaryInfo.military) {
alertedAircraft[icao] = 'military';
playAlertSound('military');
@@ -278,6 +288,114 @@
alertsEnabled = document.getElementById('alertToggle').checked;
}
// ============================================
// WATCHLIST FUNCTIONS
// ============================================
function saveWatchlist() {
localStorage.setItem('adsb_watchlist', JSON.stringify(watchlist));
}
function isOnWatchlist(aircraft) {
const icao = aircraft.icao?.toUpperCase();
const callsign = aircraft.callsign?.toUpperCase()?.trim();
const registration = aircraft.registration?.toUpperCase()?.trim();
return watchlist.some(entry => {
const val = entry.value.toUpperCase();
if (entry.type === 'icao' && icao === val) return true;
if (entry.type === 'callsign' && callsign && callsign.includes(val)) return true;
if (entry.type === 'registration' && registration === val) return true;
if (entry.type === 'any') {
if (icao === val) return true;
if (callsign && callsign.includes(val)) return true;
if (registration === val) return true;
}
return false;
});
}
function addToWatchlist(value, type = 'any', note = '') {
value = value.trim().toUpperCase();
if (!value) return false;
// Check for duplicates
const exists = watchlist.some(e => e.value.toUpperCase() === value && e.type === type);
if (exists) return false;
watchlist.push({ value, type, note, added: Date.now() });
saveWatchlist();
renderWatchlist();
return true;
}
function removeFromWatchlist(index) {
watchlist.splice(index, 1);
saveWatchlist();
renderWatchlist();
}
function showWatchlistModal() {
renderWatchlist();
document.getElementById('watchlistModal').classList.add('active');
document.getElementById('watchlistInput').focus();
}
function closeWatchlistModal() {
document.getElementById('watchlistModal').classList.remove('active');
}
function renderWatchlist() {
const container = document.getElementById('watchlistEntries');
document.getElementById('watchlistCount').textContent = watchlist.length;
if (watchlist.length === 0) {
container.innerHTML = '<div class="watchlist-empty">No entries. Add callsigns, registrations, or ICAO codes to watch.</div>';
return;
}
container.innerHTML = watchlist.map((entry, i) => `
<div class="watchlist-entry">
<div class="watchlist-entry-info">
<span class="watchlist-value">${entry.value}</span>
<span class="watchlist-type">${entry.type}</span>
${entry.note ? `<span class="watchlist-note">${entry.note}</span>` : ''}
</div>
<button class="watchlist-remove" onclick="removeFromWatchlist(${i})" title="Remove">&times;</button>
</div>
`).join('');
}
// Close watchlist modal on overlay click or Escape key
document.getElementById('watchlistModal')?.addEventListener('click', (e) => {
if (e.target.id === 'watchlistModal') closeWatchlistModal();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeWatchlistModal();
closeSquawkModal();
}
});
function handleWatchlistAdd() {
const input = document.getElementById('watchlistInput');
const type = document.getElementById('watchlistType').value;
const note = document.getElementById('watchlistNote').value.trim();
if (addToWatchlist(input.value, type, note)) {
input.value = '';
document.getElementById('watchlistNote').value = '';
}
}
function addCurrentAircraftToWatchlist() {
if (!selectedIcao || !aircraft[selectedIcao]) return;
const ac = aircraft[selectedIcao];
const value = ac.callsign || ac.registration || ac.icao;
const type = ac.callsign ? 'callsign' : (ac.registration ? 'registration' : 'icao');
addToWatchlist(value, type);
showAlertBanner(`Added ${value} to watchlist`, '#00d4ff');
}
// ============================================
// MILITARY/EMERGENCY DETECTION
// ============================================
@@ -1020,6 +1138,7 @@
if (currentFilter === 'military') return militaryInfo.military;
if (currentFilter === 'civil') return !militaryInfo.military;
if (currentFilter === 'emergency') return !!squawkInfo;
if (currentFilter === 'watchlist') return isOnWatchlist(ac);
return true;
}
@@ -1512,7 +1631,7 @@ sudo make install</code>
sortedAircraft.forEach(ac => {
const div = document.createElement('div');
div.className = `aircraft-item ${selectedIcao === ac.icao ? 'selected' : ''}`;
div.className = `aircraft-item ${selectedIcao === ac.icao ? 'selected' : ''} ${isOnWatchlist(ac) ? 'watched' : ''}`;
div.setAttribute('data-icao', ac.icao);
div.onclick = () => selectAircraft(ac.icao);
div.innerHTML = buildAircraftItemHTML(ac);
@@ -1531,7 +1650,7 @@ sudo make install</code>
sortedAircraft.forEach(ac => {
const existingItem = existingItems[ac.icao];
if (existingItem) {
existingItem.className = `aircraft-item ${selectedIcao === ac.icao ? 'selected' : ''}`;
existingItem.className = `aircraft-item ${selectedIcao === ac.icao ? 'selected' : ''} ${isOnWatchlist(ac) ? 'watched' : ''}`;
existingItem.innerHTML = buildAircraftItemHTML(ac);
}
});
@@ -2180,6 +2299,34 @@ sudo make install</code>
</div>
</div>
<!-- Watchlist Modal -->
<div id="watchlistModal" class="watchlist-modal">
<div class="watchlist-modal-content">
<div class="watchlist-modal-header">
<span>★ WATCHLIST</span>
<button class="watchlist-modal-close" onclick="closeWatchlistModal()">&times;</button>
</div>
<div class="watchlist-add-form">
<input type="text" id="watchlistInput" placeholder="Callsign, registration, or ICAO..." onkeypress="if(event.key==='Enter')handleWatchlistAdd()">
<select id="watchlistType">
<option value="any">Any match</option>
<option value="callsign">Callsign</option>
<option value="registration">Registration</option>
<option value="icao">ICAO Hex</option>
</select>
<input type="text" id="watchlistNote" placeholder="Note (optional)" style="flex:1;">
<button onclick="handleWatchlistAdd()">ADD</button>
</div>
<div class="watchlist-entries" id="watchlistEntries">
<div class="watchlist-empty">No entries. Add callsigns, registrations, or ICAO codes to watch.</div>
</div>
<div class="watchlist-footer">
<span class="watchlist-count"><span id="watchlistCount">0</span> entries</span>
<span class="watchlist-hint">Alerts trigger when watched aircraft appear</span>
</div>
</div>
</div>
<style>
.squawk-clickable {
cursor: pointer;
@@ -2310,6 +2457,189 @@ sudo make install</code>
padding: 4px;
border-bottom: 1px solid var(--border-color, #444);
}
/* Watchlist Button */
.watchlist-btn {
background: transparent;
border: 1px solid var(--border-color, #444);
color: var(--accent-cyan, #00d4ff);
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.watchlist-btn:hover {
background: var(--accent-cyan, #00d4ff);
color: #000;
}
/* Watchlist Modal */
.watchlist-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.watchlist-modal.active {
display: flex;
}
.watchlist-modal-content {
background: var(--bg-secondary, #1a1a1a);
border: 1px solid var(--border-color, #333);
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 70vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.watchlist-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bg-tertiary, #252525);
border-bottom: 1px solid var(--border-color, #333);
font-weight: 600;
font-size: 12px;
letter-spacing: 0.1em;
color: var(--accent-cyan, #00d4ff);
}
.watchlist-modal-close {
background: none;
border: none;
color: var(--text-secondary, #888);
font-size: 20px;
cursor: pointer;
}
.watchlist-modal-close:hover {
color: #fff;
}
.watchlist-add-form {
display: flex;
gap: 8px;
padding: 12px;
background: var(--bg-tertiary, #252525);
border-bottom: 1px solid var(--border-color, #333);
flex-wrap: wrap;
}
.watchlist-add-form input,
.watchlist-add-form select {
padding: 6px 10px;
border: 1px solid var(--border-color, #444);
border-radius: 4px;
background: var(--bg-primary, #0d0d0d);
color: var(--text-primary, #fff);
font-size: 12px;
}
.watchlist-add-form input:first-child {
flex: 2;
min-width: 150px;
}
.watchlist-add-form button {
padding: 6px 16px;
background: var(--accent-cyan, #00d4ff);
color: #000;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 11px;
}
.watchlist-add-form button:hover {
opacity: 0.9;
}
.watchlist-entries {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.watchlist-empty {
text-align: center;
color: var(--text-secondary, #666);
padding: 30px;
font-size: 12px;
}
.watchlist-entry {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: var(--bg-tertiary, #252525);
border-radius: 4px;
margin-bottom: 6px;
}
.watchlist-entry:hover {
background: var(--bg-primary, #1a1a1a);
}
.watchlist-entry-info {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.watchlist-value {
font-family: 'JetBrains Mono', monospace;
font-weight: bold;
color: var(--accent-cyan, #00d4ff);
font-size: 13px;
}
.watchlist-type {
font-size: 9px;
padding: 2px 6px;
background: var(--bg-primary, #0d0d0d);
border-radius: 3px;
color: var(--text-secondary, #888);
text-transform: uppercase;
}
.watchlist-note {
font-size: 11px;
color: var(--text-secondary, #888);
}
.watchlist-remove {
background: none;
border: none;
color: var(--text-secondary, #666);
font-size: 18px;
cursor: pointer;
padding: 0 4px;
}
.watchlist-remove:hover {
color: #ff4444;
}
.watchlist-footer {
display: flex;
justify-content: space-between;
padding: 10px 16px;
background: var(--bg-tertiary, #252525);
border-top: 1px solid var(--border-color, #333);
font-size: 10px;
color: var(--text-secondary, #888);
}
.watchlist-hint {
opacity: 0.7;
}
/* Watched aircraft highlight in list */
.aircraft-item.watched {
border-left: 3px solid var(--accent-cyan, #00d4ff);
}
.aircraft-item.watched::before {
content: '★';
position: absolute;
right: 8px;
top: 8px;
color: var(--accent-cyan, #00d4ff);
font-size: 10px;
}
</style>
</body>
</html>