Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
42cde48
feat: add notification statistics (types, airlines, totals)
PearMan95 May 28, 2026
598023e
feat: update privacy policy and enhance statistics display in history…
PearMan95 May 28, 2026
2303c22
Merge pull request #3 from PearMan95/feature/statistics
PearMan95 May 28, 2026
d7a60fc
refactor(settings): restructure cards, split Sound, improve notif pre…
PearMan95 May 29, 2026
fe2506d
Merge pull request #5 from PearMan95/fix/settings-restructure
PearMan95 May 29, 2026
ee0a9e3
Implement code changes to enhance functionality and improve performance
PearMan95 May 29, 2026
c08dba7
refactor(css): consolidate variables, remove dead rules,
PearMan95 Jun 2, 2026
b10ccd8
docs: update README to v1.5.0, clarify notifHistory in PRIVACY, add b…
PearMan95 Jun 3, 2026
8ed4bb4
Merge pull request #6 from PearMan95/feature/docs-and-ux-polish
PearMan95 Jun 3, 2026
489c6f0
feat: add toast on alert add, move showSaved to popup.js, add release…
PearMan95 Jun 3, 2026
422a6ef
Merge pull request #7 from PearMan95/feature/ux-feedback
PearMan95 Jun 3, 2026
aa14645
perf: merge storage reads in buildDropdownContent, debounce storage.o…
PearMan95 Jun 4, 2026
390e307
Merge pull request #9 from PearMan95/perf/live-tab-optimizations
PearMan95 Jun 4, 2026
8c60d2f
fix: escape externally-sourced data before DOM insertion
PearMan95 Jul 16, 2026
021ce9a
chore: remove unneeded web_accessible_resources, encode hex in URLs
PearMan95 Jul 16, 2026
e401aed
Merge pull request #11 from PearMan95/security/hardening-web-resource…
PearMan95 Jul 16, 2026
eb6e472
add: added fonts for offline font usage
PearMan95 Jul 16, 2026
b5478b0
add: include local font definitions for Space Mono and Outfit in popu…
PearMan95 Jul 17, 2026
a5acff4
chore: add explicit CSP to manifest.json
PearMan95 Jul 17, 2026
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.
42 changes: 39 additions & 3 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 Expand Up @@ -175,7 +211,7 @@ chrome.notifications.onButtonClicked.addListener(async (notifId, btnIdx) => {
if (btnIdx === 0) {
const hexParts = notifId.split('_');
const hex = hexParts.length >= 3 ? hexParts[1] : '';
chrome.tabs.create({ url: `https://globe.airplanes.live${hex ? `/?icao=${hex}` : ''}` });
chrome.tabs.create({ url: `https://globe.airplanes.live${hex ? `/?icao=${encodeURIComponent(hex)}` : ''}` });
}
if (btnIdx === 1) {
const parts = notifId.split('_');
Expand Down
Binary file added fonts/outfit-latin-400-normal.woff2
Binary file not shown.
Binary file added fonts/outfit-latin-600-normal.woff2
Binary file not shown.
Binary file added fonts/outfit-latin-800-normal.woff2
Binary file not shown.
Binary file added fonts/space-mono-latin-400-normal.woff2
Binary file not shown.
Binary file added fonts/space-mono-latin-700-normal.woff2
Binary file not shown.
18 changes: 5 additions & 13 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Plane Alert",
"description": "Get notified when a tracked aircraft enters your area",
"version": "1.5.0",
"version": "1.6.0",
"homepage_url": "https://github.com/PearMan95/plane-alert",
"permissions": [
"storage",
Expand All @@ -14,6 +14,9 @@
"host_permissions": [
"https://api.airplanes.live/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; font-src 'self'; style-src 'self' 'unsafe-inline'"
},
"background": {
"service_worker": "background.js"
},
Expand All @@ -29,16 +32,5 @@
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"web_accessible_resources": [
{
"resources": [
"offscreen.html",
"offscreen.js"
],
"matches": [
"chrome-extension://*/*"
]
}
]
}
}
69 changes: 53 additions & 16 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 @@ -62,6 +62,19 @@ const placeholders = {

// ─── EVENTS ────────────────────────────────────────────────────────────────

function setNoteDisplay(el, noteText) {
el.innerHTML = '';
el.dataset.note = noteText || '';
if (noteText) {
el.textContent = noteText;
} else {
const emptySpan = document.createElement('span');
emptySpan.className = 'alert-note-empty';
emptySpan.textContent = '+ add note';
el.appendChild(emptySpan);
}
}

function setupAlertsEvents() {

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

Expand All @@ -129,26 +143,51 @@ async function renderAlerts(alerts) {
for (const alert of alerts) {
const item = document.createElement('div');
item.className = 'alert-item';
item.innerHTML = `
<button class="alert-toggle ${alert.active ? 'on' : ''}" data-id="${alert.id}"></button>
<div class="alert-info">
<div class="alert-value">${alert.type === 'dbflag' ? (flagLabels[alert.value] || alert.value) : alert.value}</div>
<div class="alert-type-label">${typeLabels[alert.type] || alert.type}</div>
<div class="alert-note" data-id="${alert.id}" title="Click to edit note">${alert.note || '<span class="alert-note-empty">+ add note</span>'}</div>
</div>
<button class="btn-remove" data-id="${alert.id}">X</button>
`;

const toggleBtn = document.createElement('button');
toggleBtn.className = `alert-toggle ${alert.active ? 'on' : ''}`;
toggleBtn.dataset.id = alert.id;

const info = document.createElement('div');
info.className = 'alert-info';

const valueDiv = document.createElement('div');
valueDiv.className = 'alert-value';
valueDiv.textContent = alert.type === 'dbflag' ? (flagLabels[alert.value] || alert.value) : alert.value;

const typeDiv = document.createElement('div');
typeDiv.className = 'alert-type-label';
typeDiv.textContent = typeLabels[alert.type] || alert.type;

const noteDiv = document.createElement('div');
noteDiv.className = 'alert-note';
noteDiv.dataset.id = alert.id;
noteDiv.title = 'Click to edit note';
setNoteDisplay(noteDiv, alert.note);

info.appendChild(valueDiv);
info.appendChild(typeDiv);
info.appendChild(noteDiv);

const removeBtn = document.createElement('button');
removeBtn.className = 'btn-remove';
removeBtn.dataset.id = alert.id;
removeBtn.textContent = 'X';

item.appendChild(toggleBtn);
item.appendChild(info);
item.appendChild(removeBtn);
list.appendChild(item);
}

// Inline note bewerken
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();
const currentNote = el.dataset.note || '';

const input = document.createElement('input');
input.type = 'text';
Expand All @@ -169,15 +208,13 @@ async function renderAlerts(alerts) {
alert.note = newNote;
await chrome.storage.local.set({ alerts });
}
el.innerHTML = newNote
? newNote
: '<span class="alert-note-empty">+ add note</span>';
setNoteDisplay(el, newNote);
}

input.addEventListener('blur', saveNote);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { input.blur(); }
if (e.key === 'Escape') { el.innerHTML = currentNote || '<span class="alert-note-empty">+ add note</span>'; }
if (e.key === 'Escape') { setNoteDisplay(el, currentNote); }
});
});
});
Expand Down
Loading