mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 06:01:56 -07:00
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:
@@ -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;
|
||||
|
||||
@@ -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">×</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()">×</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>
|
||||
|
||||
Reference in New Issue
Block a user