const FLIGHT_SERVER = "https://pas-flight-server-new.dsouza-neil2011.workers.dev";
const STALE_AFTER_MS = 30000; // hide aircraft that haven't reported for 30s
const POLL_MS = 3000;
const aircraftMarkers = {};
const flightMap = L.map("flight-map", {
zoomControl: true,
attributionControl: true
}).setView([-33.86, 151.21], 9);
const radarSweep = document.createElement("div");
radarSweep.className = "radar-sweep";
flightMap.getContainer().appendChild(radarSweep);
L.tileLayer("https://tile.openstreetmap.de/{z}/{x}/{y}.png", {
maxZoom: 18,
attribution: "© OpenStreetMap contributors"
}).addTo(flightMap);
const airportLayer = L.layerGroup().addTo(flightMap);
const runwayLayer = L.layerGroup().addTo(flightMap);
const runwayLabelLayer = L.layerGroup(); // only shown when zoomed in (thousands of labels are slow)
function updateLabelVisibility() {
const show = flightMap.getZoom() >= 10;
if (show && !flightMap.hasLayer(runwayLabelLayer)) runwayLabelLayer.addTo(flightMap);
if (!show && flightMap.hasLayer(runwayLabelLayer)) flightMap.removeLayer(runwayLabelLayer);
}
flightMap.on("zoomend", updateLabelVisibility);
// ---------- helpers ----------
// Flight data comes from other people's browsers: never put it into innerHTML unescaped
function escapeHTML(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Proper CSV parser (handles quoted fields containing commas, e.g. airport names)
function parseCSV(text) {
const rows = [];
let row = [], field = "", inQuotes = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (inQuotes) {
if (c === '"' && text[i + 1] === '"') { field += '"'; i++; }
else if (c === '"') inQuotes = false;
else field += c;
} else if (c === '"') {
inQuotes = true;
} else if (c === ",") {
row.push(field); field = "";
} else if (c === "\n") {
row.push(field); field = "";
rows.push(row); row = [];
} else if (c !== "\r") {
field += c;
}
}
if (field || row.length) { row.push(field); rows.push(row); }
return rows;
}
async function loadCSV(path) {
const response = await fetch(path);
if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`);
const rows = parseCSV(await response.text());
const headers = rows.shift() || [];
const index = (name) => headers.indexOf(name);
return { rows, index };
}
// ---------- ATC messages (admin only) ----------
const atcInput = document.getElementById("atc-message");
const sendButton = document.getElementById("send-atc");
const panelStatus = document.querySelector(".panel-status");
function adminToken() {
return sessionStorage.getItem("pasAdminToken");
}
if (!adminToken()) {
// Sending is also enforced by the server; this just makes the UI honest
atcInput.disabled = true;
sendButton.disabled = true;
atcInput.placeholder = "Admin login required to send ATC instructions";
}
sendButton.addEventListener("click", sendATCMessage);
atcInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") sendATCMessage();
});
async function sendATCMessage() {
const message = atcInput.value.trim();
if (!message || !adminToken()) return;
sendButton.disabled = true;
sendButton.textContent = "SENDING...";
try {
const response = await fetch(`${FLIGHT_SERVER}/atc`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${adminToken()}`
},
body: JSON.stringify({ message })
});
const result = await response.json().catch(() => ({}));
if (response.ok && result.success) {
panelStatus.textContent = "MESSAGE SENT";
panelStatus.style.color = "#58b0ff";
atcInput.value = "";
} else if (response.status === 401) {
sessionStorage.removeItem("pasAdminToken");
panelStatus.textContent = "SESSION EXPIRED — LOG IN AGAIN";
panelStatus.style.color = "#ff6b6b";
} else {
panelStatus.textContent = "SEND FAILED";
panelStatus.style.color = "#ff6b6b";
}
setTimeout(() => {
panelStatus.textContent = "STANDBY";
panelStatus.style.color = "";
}, 2500);
} catch (error) {
console.error("ATC SEND ERROR:", error);
panelStatus.textContent = "CONNECTION ERROR";
panelStatus.style.color = "#ff6b6b";
} finally {
sendButton.disabled = !adminToken();
sendButton.textContent = "SEND →";
}
}
// ---------- live aircraft ----------
let loadingAircraft = false;
function makeAircraftIcon() {
return L.divIcon({
className: "aircraft-marker",
html: `
`,
iconSize: [44, 44],
iconAnchor: [22, 22]
});
}
async function loadAircraft() {
if (loadingAircraft) return; // don't stack requests if the server is slow
loadingAircraft = true;
try {
const response = await fetch(FLIGHT_SERVER);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const flights = await response.json();
const now = Date.now();
const activeFlights = (Array.isArray(flights) ? flights : []).filter((flight) =>
flight &&
flight.pilotId &&
Array.isArray(flight.position) &&
Number.isFinite(Number(flight.position[0])) &&
Number.isFinite(Number(flight.position[1])) &&
// drop stale entries so a pilot who left doesn't leave a frozen plane behind
(!flight.updatedAt || now - Number(flight.updatedAt) < STALE_AFTER_MS)
);
document.getElementById("pilot-count").textContent = activeFlights.length;
// ----- side panel -----
const aircraftList = document.getElementById("aircraft-list");
aircraftList.innerHTML = "";
if (activeFlights.length === 0) {
aircraftList.innerHTML = `