Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Privacy Policy

**Last updated: March 18, 2026**
**Last updated: June 4, 2026**

## Overview

Expand All @@ -14,7 +14,8 @@ Plane Alert collects and stores the following data locally on your device:
- **Alert settings** — the alerts you configure (e.g. registrations, flight numbers, aircraft types).
- **Notification preferences** — your notification and sound settings.
- **Caught aircraft** — ICAO hex addresses of aircraft you have marked as caught.
- **Notification history** — a local log of triggered alerts (callsign, timestamp, flight details).
- **Notification history** — a local log of triggered alerts (callsign, timestamp, flight details). This log is stored only on your current device and is intentionally excluded from backup exports, as it reflects activity specific to your device and is not meaningful to restore.
- **Statistics** — all-time counters derived from triggered notifications: total notification count, date of first detection, aircraft type counts, and airline prefix counts. These are stored locally and can be reset at any time via the History tab.

## How Your Data Is Used

Expand All @@ -24,6 +25,12 @@ All data is used solely to provide the core functionality of the extension: poll

All data is stored **locally on your device**, so nothing is shared with the developer. With one exception: your approximate location and radius are sent to the airplanes.live API with each poll to retrieve nearby aircraft.

## Backup & Export

The backup export (Settings → Backup) includes: alerts, location, radius, units, notification preferences, sound settings, startup tab, caught aircraft, and statistics.

**Notification history is excluded from backups by design.** It is an ephemeral device-local log and carries no value when restored to a different device or after a reinstall.

## Third-Party Services

Plane Alert uses the [airplanes.live](https://airplanes.live) API to retrieve real-time aircraft data. Your approximate location (latitude, longitude, radius) is sent to this API with each poll. Please refer to the [airplanes.live](https://airplanes.live) website for their own privacy practices.
Expand All @@ -34,7 +41,7 @@ We do not sell, trade, or share any of your data with third parties.

## Data Deletion

You can delete all locally stored data at any time by removing the extension from Chrome, or by using the "Export backup" feature and clearing your settings manually.
You can delete all locally stored data at any time by removing the extension from Chrome, or by using the "Export backup" feature and clearing your settings manually. Notification history and statistics can be cleared independently via the History tab.

## Changes to This Policy

Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A Chrome extension that notifies you when a tracked aircraft enters your radius — powered by the [airplanes.live](https://airplanes.live) API.

![Version](https://img.shields.io/badge/version-1.4.0-blue)
![Version](https://img.shields.io/badge/version-1.5.0-blue)
![Manifest](https://img.shields.io/badge/manifest-v3-green)
![License](https://img.shields.io/badge/license-GPL%20v3-lightgrey)

Expand All @@ -15,7 +15,7 @@ A Chrome extension that notifies you when a tracked aircraft enters your radius
- **Catch aircraft** — mark an aircraft as caught directly from the notification or the Live tab detail panel. Caught aircraft won't trigger future notifications. Manage your caught list in the History tab.
- **Alert sound** — choose from Ping, Radar, Alert or Chime with adjustable volume (soft, medium, loud)
- **Live tab** — see all aircraft in your radius in real time, with sorting and filtering options
- **Detail dropdown** — click any aircraft to expand its details inline: registration, type, altitude, speed, route, squawk and heading
- **Detail dropdown** — click any aircraft to expand its details inline: registration, type, altitude, speed, route, squawk and heading. Use the 🔔/🔕 bell buttons to toggle alerts for that registration or type directly from the dropdown.
- **Notification history** — a log of every triggered alert with callsign, time and flight details
- **Settings** — organised into collapsible cards: Location, Filters, Units, Notifications, Startup tab, and Backup & Test
- **Startup tab** — choose which tab opens on launch, or always return to the last tab you were on
Expand Down Expand Up @@ -60,18 +60,32 @@ Plane Alert polls the airplanes.live API in the background every minute. When an

---

## Data & privacy

All data is stored locally on your device. The only external call is to the airplanes.live API, which receives your approximate location and radius with each poll.

**What is included in a backup export:** alerts, location, radius, units, notification preferences, sound settings, startup tab, caught aircraft, and statistics.

**What is intentionally excluded from backups:** notification history (`notifHistory`). This log is ephemeral by design — it reflects what happened on your device and is not meaningful to restore on another machine or after a reinstall.

See [PRIVACY.md](PRIVACY.md) for the full privacy policy.

---

## File structure

```
plane-alert/
├── manifest.json
├── background.js — background service worker, polling & notifications
├── shared.js — shared match logic (used by background & popup)
├── offscreen.html — offscreen document for audio playback
├── offscreen.js — Web Audio API sound engine
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
│ ├── icon128.png
│ └── airplanes-live-logo.png
└── popup/
├── popup.html — extension UI skeleton
├── popup.css — all styles
Expand Down Expand Up @@ -104,4 +118,4 @@ This project is licensed under the **GNU General Public License v3.0**.

You are free to view, modify and redistribute the source code, but any derivative work must also be open source under the same license.

**Publishing this extension or any derivative to a browser extension store (Chrome, Firefox, Edge, etc.) requires explicit written permission from the author.** See the [LICENSE](LICENSE) file for full details.
**Publishing this extension or any derivative to a browser extension store (Chrome, Firefox, Edge, etc.) requires explicit written permission from the author.** See the [LICENSE](LICENSE) file for full details.
40 changes: 38 additions & 2 deletions background.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// background.js — runs in the background and polls the API
// background.js v1.1.0 — runs in the background and polls the API
// Depends on shared.js (loaded via manifest.json background.scripts)

// ─── OFFSCREEN DOCUMENT ────────────────────────────────────────────────────
Expand Down Expand Up @@ -72,6 +72,40 @@ chrome.runtime.onMessage.addListener((msg) => {
}
});

// ─── STATISTIEKEN BIJHOUDEN ────────────────────────────────────────────────

async function recordStats(ac) {
const keys = ['statsTotalCount', 'statsFirstDetection', 'statsTypeCounts', 'statsAirlineCounts'];
const data = await chrome.storage.local.get(keys);

const totalCount = (data.statsTotalCount || 0) + 1;
const firstDetect = data.statsFirstDetection || new Date().toISOString();
const typeCounts = data.statsTypeCounts || {};
const airlineCounts = data.statsAirlineCounts || {};

// Type tellen (alleen als bekend)
if (ac.t) {
const t = ac.t.toUpperCase().trim();
typeCounts[t] = (typeCounts[t] || 0) + 1;
}

// Airline tellen: eerste 3 letters van vluchtcode, alleen als vlucht minstens 4 tekens heeft
if (ac.flight && ac.flight.trim().length >= 4) {
const prefix = ac.flight.trim().toUpperCase().substring(0, 3);
// Alleen letters (geen cijfers-prefix zoals militairen of privé)
if (/^[A-Z]{3}$/.test(prefix)) {
airlineCounts[prefix] = (airlineCounts[prefix] || 0) + 1;
}
}

await chrome.storage.local.set({
statsTotalCount: totalCount,
statsFirstDetection: firstDetect,
statsTypeCounts: typeCounts,
statsAirlineCounts: airlineCounts
});
}

async function pollAircraft() {
const { enabled = true } = await chrome.storage.local.get('enabled');
if (!enabled) return;
Expand Down Expand Up @@ -106,7 +140,6 @@ async function pollAircraft() {
for (const ac of aircraft) {
if (ac.hex && caughtAircraft.includes(ac.hex)) continue;

// matchesAlert komt uit shared.js
const matchingAlert = config.alerts.find(alert => alert.active && matchesAlert(ac, alert));
if (!matchingAlert) continue;

Expand All @@ -119,6 +152,9 @@ async function pollAircraft() {
inRange[key] = true;
await chrome.storage.local.set({ inRange });

// Statistieken bijhouden voor elke nieuwe match
await recordStats(ac);

const { notificationsEnabled = true, notifShow = {} } =
await chrome.storage.local.get(['notificationsEnabled', 'notifShow']);
const show = Object.assign({ reg: false, type: true, alt: true, speed: true, route: true, dir: true }, notifShow);
Expand Down
5 changes: 3 additions & 2 deletions popup/alerts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// alerts.js — injecteert Alerts tab HTML en beheert alert logica
// alerts.js v1.1.0 — injecteert Alerts tab HTML en beheert alert logica

// ─── HTML INJECTIE ──────────────────────────────────────────────────────────

Expand Down Expand Up @@ -108,6 +108,7 @@ function setupAlertsEvents() {
document.getElementById('alertValue').value = '';
document.getElementById('alertNote').value = '';
renderAlerts(alerts);
showSaved('Alert added');
});
}

Expand Down Expand Up @@ -145,7 +146,7 @@ async function renderAlerts(alerts) {
list.querySelectorAll('.alert-note').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
if (el.querySelector('input')) return; // al in edit mode
if (el.querySelector('input')) return;

const id = el.dataset.id;
const currentNote = el.textContent.trim() === '+ add note' ? '' : el.textContent.trim();
Expand Down
139 changes: 138 additions & 1 deletion popup/history.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
// history.js — injecteert History tab HTML en beheert notificatiegeschiedenis
// history.js v1.2.0 — injecteert History tab HTML en beheert notificatiegeschiedenis

// ─── HTML INJECTIE ──────────────────────────────────────────────────────────

function initHistoryTab() {
document.getElementById('tab-history').innerHTML = `

<!-- 📊 Statistieken -->
<div class="stats-dropdown" id="statsDropdown">
<button id="btnToggleStats" class="stats-toggle-btn">
<span>📊 Statistics</span>
<span id="statsChevron" class="stats-chevron">▼</span>
</button>
<div id="statsPanel" style="display:none">
<div class="stats-panel-inner" id="statsPanelInner">
<div class="empty-state" style="padding:12px 0">Loading…</div>
</div>
<button class="btn-clear-history" id="btnResetStats" style="width:100%">Reset statistics</button>
</div>
</div>

<!-- Notificatiegeschiedenis -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<div class="section-label" style="margin:0">Notification history</div>
<button class="btn-clear-history" id="btnClearHistory">Clear</button>
Expand Down Expand Up @@ -44,9 +60,130 @@ function setupHistoryEvents() {
});

document.getElementById('btnClearCaught').addEventListener('click', async () => {
const btn = document.getElementById('btnClearCaught');
if (btn.dataset.confirm !== '1') {
btn.dataset.confirm = '1';
btn.textContent = 'Sure? Click again to confirm';
btn.style.color = '#ef4444';
btn.style.borderColor = '#ef4444';
setTimeout(() => {
if (btn.dataset.confirm === '1') {
btn.dataset.confirm = '';
btn.textContent = 'Release all';
btn.style.color = '';
btn.style.borderColor = '';
}
}, 2500);
return;
}
btn.dataset.confirm = '';
btn.textContent = 'Release all';
btn.style.color = '';
btn.style.borderColor = '';
await chrome.storage.local.set({ caughtAircraft: [], caughtAircraftLabels: {} });
renderCaughtList();
});

document.getElementById('btnToggleStats').addEventListener('click', () => {
const panel = document.getElementById('statsPanel');
const chevron = document.getElementById('statsChevron');
const isOpen = panel.style.display !== 'none';
panel.style.display = isOpen ? 'none' : 'block';
chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
if (!isOpen) renderStats();
});

document.getElementById('btnResetStats').addEventListener('click', async () => {
const btn = document.getElementById('btnResetStats');
if (btn.dataset.confirm !== '1') {
btn.dataset.confirm = '1';
btn.textContent = 'Sure? Click again to confirm';
btn.style.color = '#ef4444';
btn.style.borderColor = '#ef4444';
setTimeout(() => {
btn.dataset.confirm = '';
btn.textContent = 'Reset statistics';
btn.style.color = '';
btn.style.borderColor = '';
}, 2500);
return;
}
await chrome.storage.local.remove([
'statsTotalCount', 'statsTypeCounts', 'statsAirlineCounts'
]);
btn.dataset.confirm = '';
btn.textContent = 'Reset statistics';
btn.style.color = '';
btn.style.borderColor = '';
renderStats();
});
}

// ─── STATISTIEKEN RENDEREN ─────────────────────────────────────────────────

async function renderStats() {
const container = document.getElementById('statsPanelInner');
if (!container) return;

const { statsTotalCount = 0, statsFirstDetection, statsTypeCounts = {}, statsAirlineCounts = {} } =
await chrome.storage.local.get(['statsTotalCount', 'statsFirstDetection', 'statsTypeCounts', 'statsAirlineCounts']);

if (statsTotalCount === 0 && !statsFirstDetection) {
container.innerHTML = '<div style="font-family:Space Mono,monospace;font-size:10px;color:#8b9cc8;padding:4px 0 10px">No data yet. Stats are recorded once notifications start firing.</div>';
return;
}

function topN(obj, n) {
return Object.entries(obj)
.sort((a, b) => b[1] - a[1])
.slice(0, n);
}

const topTypes = topN(statsTypeCounts, 5);
const topAirlines = topN(statsAirlineCounts, 5);

const firstDate = statsFirstDetection
? new Date(statsFirstDetection).toLocaleDateString([], { day: '2-digit', month: 'short', year: 'numeric' })
: '—';

function barRow(label, count, max) {
const pct = max > 0 ? Math.round((count / max) * 100) : 0;
return `
<div class="stats-bar-row">
<span class="stats-bar-label">${label}</span>
<div class="stats-bar-track">
<div class="stats-bar-fill" style="width:${pct}%"></div>
</div>
<span class="stats-bar-count">${count}</span>
</div>
`;
}

const maxType = topTypes[0]?.[1] || 1;
const maxAirline = topAirlines[0]?.[1] || 1;

container.innerHTML = `
<div class="stats-meta-row">
<div class="stats-meta-cell">
<div class="stats-meta-label">Total notifications</div>
<div class="stats-meta-value">${statsTotalCount}</div>
</div>
<div class="stats-meta-cell">
<div class="stats-meta-label">First detection</div>
<div class="stats-meta-value" style="font-size:11px">${firstDate}</div>
</div>
</div>

${topTypes.length > 0 ? `
<div class="stats-section-label">Top aircraft types</div>
${topTypes.map(([t, c]) => barRow(t, c, maxType)).join('')}
` : ''}

${topAirlines.length > 0 ? `
<div class="stats-section-label" style="margin-top:10px">Top airlines</div>
${topAirlines.map(([a, c]) => barRow(a, c, maxAirline)).join('')}
` : ''}
`;
}

async function renderCaughtList() {
Expand Down
Loading