From 10e535684d4a3eefa1947a57a1d9f35759c379b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:54:24 +0000 Subject: [PATCH 01/31] Initial plan From 39f2775f9f39c8e766d87b53cb845be5b94f7b4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:10:48 +0000 Subject: [PATCH 02/31] Add comprehensive debug logging with toggle control Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 180 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 149 insertions(+), 31 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index fe7b696..e20435f 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -7,6 +7,28 @@ import { WebBleConnection } from "./mc/index.js"; // your BLE client +// ---- Debug Configuration ---- +const DEBUG_ENABLED = true; // Set to false to disable all debug logging + +// Debug logging helper function +function debugLog(message, ...args) { + if (DEBUG_ENABLED) { + console.log(`[DEBUG] ${message}`, ...args); + } +} + +function debugWarn(message, ...args) { + if (DEBUG_ENABLED) { + console.warn(`[DEBUG] ${message}`, ...args); + } +} + +function debugError(message, ...args) { + if (DEBUG_ENABLED) { + console.error(`[DEBUG] ${message}`, ...args); + } +} + // ---- Config ---- const CHANNEL_NAME = "#wardriving"; // change to "#wardrive" if needed const DEFAULT_INTERVAL_S = 30; // fallback if selector unavailable @@ -228,45 +250,48 @@ function setConnectButton(connected) { // ---- Wake Lock helpers ---- async function acquireWakeLock() { + debugLog("Attempting to acquire wake lock"); if (navigator.bluetooth && typeof navigator.bluetooth.setScreenDimEnabled === "function") { try { navigator.bluetooth.setScreenDimEnabled(true); state.bluefyLockEnabled = true; - console.log("Bluefy screen-dim prevention enabled"); + debugLog("Bluefy screen-dim prevention enabled"); return; } catch (e) { - console.warn("Bluefy setScreenDimEnabled failed:", e); + debugWarn("Bluefy setScreenDimEnabled failed:", e); } } try { if ("wakeLock" in navigator && typeof navigator.wakeLock.request === "function") { state.wakeLock = await navigator.wakeLock.request("screen"); - console.log("Wake lock acquired"); - state.wakeLock.addEventListener?.("release", () => console.log("Wake lock released")); + debugLog("Wake lock acquired successfully"); + state.wakeLock.addEventListener?.("release", () => debugLog("Wake lock released")); } else { - console.log("Wake Lock API not supported"); + debugLog("Wake Lock API not supported on this device"); } } catch (err) { - console.error(`Could not obtain wake lock: ${err.name}, ${err.message}`); + debugError(`Could not obtain wake lock: ${err.name}, ${err.message}`); } } async function releaseWakeLock() { + debugLog("Attempting to release wake lock"); if (state.bluefyLockEnabled && navigator.bluetooth && typeof navigator.bluetooth.setScreenDimEnabled === "function") { try { navigator.bluetooth.setScreenDimEnabled(false); state.bluefyLockEnabled = false; - console.log("Bluefy screen-dim prevention disabled"); + debugLog("Bluefy screen-dim prevention disabled"); } catch (e) { - console.warn("Bluefy setScreenDimEnabled(false) failed:", e); + debugWarn("Bluefy setScreenDimEnabled(false) failed:", e); } } try { if (state.wakeLock) { await state.wakeLock.release?.(); state.wakeLock = null; + debugLog("Wake lock released successfully"); } } catch (e) { - console.warn("Error releasing wake lock:", e); + debugWarn("Error releasing wake lock:", e); state.wakeLock = null; } } @@ -331,15 +356,23 @@ function stopGpsAgeUpdater() { } } function startGeoWatch() { - if (state.geoWatchId) return; - if (!("geolocation" in navigator)) return; + if (state.geoWatchId) { + debugLog("GPS watch already running, skipping start"); + return; + } + if (!("geolocation" in navigator)) { + debugError("Geolocation not available in navigator"); + return; + } + debugLog("Starting GPS watch"); state.gpsState = "acquiring"; updateGpsUi(); startGpsAgeUpdater(); // Start the age counter state.geoWatchId = navigator.geolocation.watchPosition( (pos) => { + debugLog(`GPS fix acquired: lat=${pos.coords.latitude.toFixed(5)}, lon=${pos.coords.longitude.toFixed(5)}, accuracy=${pos.coords.accuracy}m`); state.lastFix = { lat: pos.coords.latitude, lon: pos.coords.longitude, @@ -350,7 +383,7 @@ function startGeoWatch() { updateGpsUi(); }, (err) => { - console.warn("watchPosition error:", err); + debugError(`GPS watch error: ${err.code} - ${err.message}`); state.gpsState = "error"; // Keep UI honest if it fails updateGpsUi(); @@ -363,12 +396,17 @@ function startGeoWatch() { ); } function stopGeoWatch() { - if (!state.geoWatchId) return; + if (!state.geoWatchId) { + debugLog("No GPS watch to stop"); + return; + } + debugLog("Stopping GPS watch"); navigator.geolocation.clearWatch(state.geoWatchId); state.geoWatchId = null; stopGpsAgeUpdater(); // Stop the age counter } async function primeGpsOnce() { + debugLog("Priming GPS with initial position request"); // Start continuous watch so the UI keeps updating startGeoWatch(); @@ -378,6 +416,7 @@ async function primeGpsOnce() { try { const pos = await getCurrentPosition(); + debugLog(`Initial GPS position acquired: lat=${pos.coords.latitude.toFixed(5)}, lon=${pos.coords.longitude.toFixed(5)}, accuracy=${pos.coords.accuracy}m`); state.lastFix = { lat: pos.coords.latitude, lon: pos.coords.longitude, @@ -390,14 +429,17 @@ async function primeGpsOnce() { // Only refresh the coverage map if we have an accurate fix if (state.lastFix.accM && state.lastFix.accM < GPS_ACCURACY_THRESHOLD_M) { + debugLog(`GPS accuracy ${state.lastFix.accM}m is within threshold, refreshing coverage map`); scheduleCoverageRefresh( state.lastFix.lat, state.lastFix.lon ); + } else { + debugLog(`GPS accuracy ${state.lastFix.accM}m exceeds threshold (${GPS_ACCURACY_THRESHOLD_M}m), skipping map refresh`); } } catch (e) { - console.warn("primeGpsOnce failed:", e); + debugError(`primeGpsOnce failed: ${e.message}`); state.gpsState = "error"; updateGpsUi(); } @@ -408,16 +450,22 @@ async function primeGpsOnce() { // ---- Channel helpers ---- async function ensureChannel() { if (!state.connection) throw new Error("Not connected"); - if (state.channel) return state.channel; + if (state.channel) { + debugLog(`Using existing channel: ${CHANNEL_NAME}`); + return state.channel; + } + debugLog(`Looking up channel: ${CHANNEL_NAME}`); const ch = await state.connection.findChannelByName(CHANNEL_NAME); if (!ch) { + debugError(`Channel ${CHANNEL_NAME} not found on device`); enableControls(false); throw new Error( `Channel ${CHANNEL_NAME} not found. Join it on your companion first.` ); } + debugLog(`Channel found: ${CHANNEL_NAME} (index: ${ch.channelIdx})`); state.channel = ch; enableControls(true); channelInfoEl.textContent = `${CHANNEL_NAME} (CH:${ch.channelIdx})`; @@ -476,7 +524,7 @@ async function postToMeshMapperAPI(lat, lon) { test: 0 }; - console.log("Posting to MeshMapper API:", { lat, lon, who: whoIdentifier, power: powerValue }); + debugLog(`Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${whoIdentifier}, power=${powerValue}`); // POST to MeshMapper API const response = await fetch(MESHMAPPER_API_URL, { @@ -488,23 +536,25 @@ async function postToMeshMapperAPI(lat, lon) { }); if (!response.ok) { - console.warn(`MeshMapper API returned status ${response.status}`); + debugWarn(`MeshMapper API returned error status ${response.status}`); } else { - console.log("Successfully posted to MeshMapper API"); + debugLog(`MeshMapper API post successful (status ${response.status})`); } } catch (error) { // Log error but don't fail the ping - console.error("Failed to post to MeshMapper API:", error); + debugError(`MeshMapper API post failed: ${error.message}`); } } // ---- Ping ---- async function sendPing(manual = false) { + debugLog(`sendPing called (manual=${manual})`); try { // Check cooldown only for manual pings if (manual && isInCooldown()) { const remainingMs = state.cooldownEndTime - Date.now(); const remainingSec = Math.ceil(remainingMs / 1000); + debugLog(`Manual ping blocked by cooldown (${remainingSec}s remaining)`); setStatus(`Please wait ${remainingSec}s before sending another ping`, "text-amber-300"); return; } @@ -523,10 +573,11 @@ async function sendPing(manual = false) { // Auto mode: use GPS watch data if (!state.lastFix) { // If no GPS fix yet in auto mode, skip this ping and wait for watch to acquire location - console.warn("Auto ping skipped: waiting for GPS fix"); + debugWarn("Auto ping skipped: no GPS fix available yet"); setStatus("Waiting for GPS fix...", "text-amber-300"); return; } + debugLog(`Using GPS watch data for auto ping: lat=${state.lastFix.lat.toFixed(5)}, lon=${state.lastFix.lon.toFixed(5)}`); lat = state.lastFix.lat; lon = state.lastFix.lon; accuracy = state.lastFix.accM; @@ -536,15 +587,18 @@ async function sendPing(manual = false) { const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; // Allow buffer beyond interval if (state.lastFix && (Date.now() - state.lastFix.tsMs) < maxAge) { + debugLog(`Using cached GPS data (age: ${Date.now() - state.lastFix.tsMs}ms)`); lat = state.lastFix.lat; lon = state.lastFix.lon; accuracy = state.lastFix.accM; } else { // Get fresh GPS coordinates for manual ping + debugLog("Requesting fresh GPS position for manual ping"); const pos = await getCurrentPosition(); lat = pos.coords.latitude; lon = pos.coords.longitude; accuracy = pos.coords.accuracy; + debugLog(`Fresh GPS acquired: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, accuracy=${accuracy}m`); state.lastFix = { lat, lon, @@ -556,11 +610,14 @@ async function sendPing(manual = false) { } const payload = buildPayload(lat, lon); + debugLog(`Sending ping to channel: "${payload}"`); const ch = await ensureChannel(); await state.connection.sendChannelTextMessage(ch.channelIdx, payload); + debugLog(`Ping sent successfully to channel ${ch.channelIdx}`); // Start cooldown period after successful ping + debugLog(`Starting ${COOLDOWN_MS}ms cooldown`); startCooldown(); // Update status after ping is sent @@ -577,9 +634,11 @@ async function sendPing(manual = false) { // Schedule MeshMapper API call with 7-second delay (non-blocking) // Clear any existing timer first if (state.meshMapperTimer) { + debugLog("Clearing existing MeshMapper timer"); clearTimeout(state.meshMapperTimer); } + debugLog(`Scheduling MeshMapper API post in ${MESHMAPPER_DELAY_MS}ms`); state.meshMapperTimer = setTimeout(async () => { // Capture accuracy in closure to ensure it's available in nested callback const capturedAccuracy = accuracy; @@ -598,7 +657,10 @@ async function sendPing(manual = false) { // Update map after API post to ensure backend updated setTimeout(() => { if (capturedAccuracy && capturedAccuracy < GPS_ACCURACY_THRESHOLD_M) { + debugLog(`Refreshing coverage map (accuracy ${capturedAccuracy}m within threshold)`); scheduleCoverageRefresh(lat, lon); + } else { + debugLog(`Skipping map refresh (accuracy ${capturedAccuracy}m exceeds threshold)`); } // Set status to idle after map update @@ -606,8 +668,10 @@ async function sendPing(manual = false) { // If in auto mode, schedule next ping. Otherwise, set to idle if (state.running) { // Schedule the next auto ping with countdown + debugLog("Scheduling next auto ping"); scheduleNextAutoPing(); } else { + debugLog("Setting status to idle"); setStatus("Idle", "text-slate-300"); } } @@ -629,22 +693,25 @@ async function sendPing(manual = false) { sessionPingsEl.scrollTop = sessionPingsEl.scrollHeight; } } catch (e) { - console.error("Ping failed:", e); + debugError(`Ping operation failed: ${e.message}`, e); setStatus(e.message || "Ping failed", "text-red-300"); } } // ---- Auto mode ---- function stopAutoPing(stopGps = false) { + debugLog(`stopAutoPing called (stopGps=${stopGps})`); // Check if we're in cooldown before stopping (unless stopGps is true for disconnect) if (!stopGps && isInCooldown()) { const remainingMs = state.cooldownEndTime - Date.now(); const remainingSec = Math.ceil(remainingMs / 1000); + debugLog(`Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); setStatus(`Please wait ${remainingSec}s before toggling auto mode`, "text-amber-300"); return; } if (state.autoTimerId) { + debugLog("Clearing auto ping timer"); clearTimeout(state.autoTimerId); state.autoTimerId = null; } @@ -658,11 +725,16 @@ function stopAutoPing(stopGps = false) { state.running = false; updateAutoButton(); releaseWakeLock(); + debugLog("Auto ping stopped"); } function scheduleNextAutoPing() { - if (!state.running) return; + if (!state.running) { + debugLog("Not scheduling next auto ping - auto mode not running"); + return; + } const intervalMs = getSelectedIntervalMs(); + debugLog(`Scheduling next auto ping in ${intervalMs}ms`); // Start countdown immediately startAutoCountdown(intervalMs); @@ -670,13 +742,16 @@ function scheduleNextAutoPing() { // Schedule the next ping state.autoTimerId = setTimeout(() => { if (state.running) { + debugLog("Auto ping timer fired, sending ping"); sendPing(false).catch(console.error); } }, intervalMs); } function startAutoPing() { + debugLog("startAutoPing called"); if (!state.connection) { + debugError("Cannot start auto ping - not connected"); alert("Connect to a MeshCore device first."); return; } @@ -685,33 +760,40 @@ function startAutoPing() { if (isInCooldown()) { const remainingMs = state.cooldownEndTime - Date.now(); const remainingSec = Math.ceil(remainingMs / 1000); + debugLog(`Auto ping start blocked by cooldown (${remainingSec}s remaining)`); setStatus(`Please wait ${remainingSec}s before toggling auto mode`, "text-amber-300"); return; } // Clean up any existing auto-ping timer (but keep GPS watch running) if (state.autoTimerId) { + debugLog("Clearing existing auto ping timer"); clearTimeout(state.autoTimerId); state.autoTimerId = null; } stopAutoCountdown(); // Start GPS watch for continuous updates + debugLog("Starting GPS watch for auto mode"); startGeoWatch(); state.running = true; updateAutoButton(); // Acquire wake lock for auto mode + debugLog("Acquiring wake lock for auto mode"); acquireWakeLock().catch(console.error); // Send first ping immediately + debugLog("Sending initial auto ping"); sendPing(false).catch(console.error); } // ---- BLE connect / disconnect ---- async function connect() { + debugLog("connect() called"); if (!("bluetooth" in navigator)) { + debugError("Web Bluetooth not supported"); alert("Web Bluetooth not supported in this browser."); return; } @@ -719,27 +801,37 @@ async function connect() { setStatus("Connecting…", "text-sky-300"); try { + debugLog("Opening BLE connection..."); const conn = await WebBleConnection.open(); state.connection = conn; + debugLog("BLE connection object created"); conn.on("connected", async () => { + debugLog("BLE connected event fired"); setStatus("Connected", "text-emerald-300"); setConnectButton(true); connectBtn.disabled = false; const selfInfo = await conn.getSelfInfo(); + debugLog(`Device info: ${selfInfo?.name || "[No device]"}`); deviceInfoEl.textContent = selfInfo?.name || "[No device]"; updateAutoButton(); - try { await conn.syncDeviceTime?.(); } catch { /* optional */ } + try { + await conn.syncDeviceTime?.(); + debugLog("Device time synced"); + } catch { + debugLog("Device time sync not available or failed"); + } try { await ensureChannel(); await primeGpsOnce(); } catch (e) { - console.error("Channel setup failed:", e); + debugError(`Channel setup failed: ${e.message}`, e); setStatus(e.message || "Channel setup failed", "text-red-300"); } }); conn.on("disconnected", () => { + debugLog("BLE disconnected event fired"); setStatus("Disconnected", "text-red-300"); setConnectButton(false); deviceInfoEl.textContent = "—"; @@ -753,10 +845,12 @@ async function connect() { // Clean up timers if (state.meshMapperTimer) { + debugLog("Clearing MeshMapper timer on disconnect"); clearTimeout(state.meshMapperTimer); state.meshMapperTimer = null; } if (state.cooldownUpdateTimer) { + debugLog("Clearing cooldown timer on disconnect"); clearTimeout(state.cooldownUpdateTimer); state.cooldownUpdateTimer = null; } @@ -767,16 +861,21 @@ async function connect() { state.lastFix = null; state.gpsState = "idle"; updateGpsUi(); + debugLog("Disconnect cleanup complete"); }); } catch (e) { - console.error("BLE connect failed:", e); + debugError(`BLE connection failed: ${e.message}`, e); setStatus("Failed to connect", "text-red-300"); connectBtn.disabled = false; } } async function disconnect() { - if (!state.connection) return; + debugLog("disconnect() called"); + if (!state.connection) { + debugLog("No connection to disconnect"); + return; + } connectBtn.disabled = true; setStatus("Disconnecting...", "text-sky-300"); @@ -784,16 +883,19 @@ async function disconnect() { try { // WebBleConnection typically exposes one of these. if (typeof state.connection.close === "function") { + debugLog("Calling connection.close()"); await state.connection.close(); } else if (typeof state.connection.disconnect === "function") { + debugLog("Calling connection.disconnect()"); await state.connection.disconnect(); } else if (typeof state.connection.device?.gatt?.disconnect === "function") { + debugLog("Calling device.gatt.disconnect()"); state.connection.device.gatt.disconnect(); } else { - console.warn("No known disconnect method on connection object"); + debugWarn("No known disconnect method on connection object"); } } catch (e) { - console.error("BLE disconnect failed:", e); + debugError(`BLE disconnect failed: ${e.message}`, e); setStatus(e.message || "Disconnect failed", "text-red-300"); } finally { connectBtn.disabled = false; @@ -804,19 +906,24 @@ async function disconnect() { // ---- Page visibility ---- document.addEventListener("visibilitychange", async () => { if (document.hidden) { + debugLog("Page visibility changed to hidden"); if (state.running) { + debugLog("Stopping auto ping due to page hidden"); stopAutoPing(true); // Ignore cooldown check when page is hidden setStatus("Lost focus, auto mode stopped", "text-amber-300"); } else { + debugLog("Releasing wake lock due to page hidden"); releaseWakeLock(); } } else { + debugLog("Page visibility changed to visible"); // On visible again, user can manually re-start Auto. } }); // ---- Bind UI & init ---- export async function onLoad() { + debugLog("wardrive.js onLoad() called - initializing"); setStatus("Disconnected", "text-red-300"); enableControls(false); updateAutoButton(); @@ -829,12 +936,16 @@ export async function onLoad() { await connect(); } } catch (e) { - console.error(e); + debugError(`Connection button error: ${e.message}`, e); setStatus(e.message || "Connection error", "text-red-300"); } }); - sendPingBtn.addEventListener("click", () => sendPing(true).catch(console.error)); + sendPingBtn.addEventListener("click", () => { + debugLog("Manual ping button clicked"); + sendPing(true).catch(console.error); + }); autoToggleBtn.addEventListener("click", () => { + debugLog("Auto toggle button clicked"); if (state.running) { stopAutoPing(); setStatus("Auto mode stopped", "text-slate-300"); @@ -844,5 +955,12 @@ export async function onLoad() { }); // Prompt location permission early (optional) - try { await getCurrentPosition(); } catch { /* will prompt at first send */ } + debugLog("Requesting initial location permission"); + try { + await getCurrentPosition(); + debugLog("Initial location permission granted"); + } catch (e) { + debugLog(`Initial location permission not granted: ${e.message}`); + } + debugLog("wardrive.js initialization complete"); } From 27e332055c9105f033d9faff202428bd3b78d944 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:12:40 +0000 Subject: [PATCH 03/31] Fix debug logging consistency and add URL parameter control Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index e20435f..3118661 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -8,7 +8,9 @@ import { WebBleConnection } from "./mc/index.js"; // your BLE client // ---- Debug Configuration ---- -const DEBUG_ENABLED = true; // Set to false to disable all debug logging +// Enable debug logging via URL parameter (?debug=true) or set default here +const urlParams = new URLSearchParams(window.location.search); +const DEBUG_ENABLED = urlParams.get('debug') === 'true' || false; // Set to true to enable debug logging by default // Debug logging helper function function debugLog(message, ...args) { @@ -217,7 +219,7 @@ function scheduleCoverageRefresh(lat, lon, delayMs = 0) { coverageRefreshTimer = setTimeout(() => { const url = buildCoverageEmbedUrl(lat, lon); - console.log("Coverage iframe URL:", url); + debugLog("Coverage iframe URL:", url); coverageFrameEl.src = url; }, delayMs); } @@ -650,7 +652,7 @@ async function sendPing(manual = false) { try { await postToMeshMapperAPI(lat, lon); } catch (error) { - console.error("MeshMapper API post failed:", error); + debugError("MeshMapper API post failed:", error); // Continue with map refresh and status update even if API fails } From 3aecae96f3c85ba0bb356bd2068006fc3192b19f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:22:21 +0000 Subject: [PATCH 04/31] Refactor wardrive.js for improved code structure and readability Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 512 +++++++++++++++++++++++++------------------- 1 file changed, 291 insertions(+), 221 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 3118661..7a3f321 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -92,104 +92,148 @@ const state = { }; // ---- UI helpers ---- -function setStatus(text, color = "text-slate-300") { +// Status colors for different states +const STATUS_COLORS = { + idle: "text-slate-300", + success: "text-emerald-300", + warning: "text-amber-300", + error: "text-red-300", + info: "text-sky-300" +}; + +function setStatus(text, color = STATUS_COLORS.idle) { statusEl.textContent = text; statusEl.className = `font-semibold ${color}`; } -function updateAutoCountdownStatus() { - if (!state.running || !state.nextAutoPingTime) { - return; + +// Countdown timer management - generalized for reuse +function createCountdownTimer(getEndTime, getStatusMessage) { + return { + timerId: null, + endTime: null, + + start(durationMs) { + this.stop(); + this.endTime = Date.now() + durationMs; + this.update(); + this.timerId = setInterval(() => this.update(), 1000); + }, + + update() { + if (!this.endTime) return; + + const remainingMs = this.endTime - Date.now(); + if (remainingMs <= 0) { + const message = getStatusMessage(0); + if (message) setStatus(message, STATUS_COLORS.info); + return; + } + + const remainingSec = Math.ceil(remainingMs / 1000); + const message = getStatusMessage(remainingSec); + if (message) setStatus(message, STATUS_COLORS.idle); + }, + + stop() { + if (this.timerId) { + clearInterval(this.timerId); + this.timerId = null; + } + this.endTime = null; + } + }; +} + +// Auto ping countdown timer +const autoCountdownTimer = createCountdownTimer( + () => state.nextAutoPingTime, + (remainingSec) => { + if (!state.running) return null; + return remainingSec === 0 + ? "Sending auto ping..." + : `Waiting for next auto ping (${remainingSec}s)`; } - - const remainingMs = state.nextAutoPingTime - Date.now(); - if (remainingMs <= 0) { - setStatus("Sending auto ping...", "text-sky-300"); - return; +); + +// API post countdown timer +const apiCountdownTimer = createCountdownTimer( + () => state.apiPostTime, + (remainingSec) => { + return remainingSec === 0 + ? "Posting to API..." + : `Wait to post API (${remainingSec}s)`; } - - const remainingSec = Math.ceil(remainingMs / 1000); - setStatus(`Waiting for next auto ping (${remainingSec}s)`, "text-slate-300"); -} +); + +// Legacy compatibility wrappers function startAutoCountdown(intervalMs) { - // Stop any existing countdown - stopAutoCountdown(); - - // Set the next ping time state.nextAutoPingTime = Date.now() + intervalMs; - - // Update immediately - updateAutoCountdownStatus(); - - // Update every second - state.autoCountdownTimer = setInterval(() => { - updateAutoCountdownStatus(); - }, 1000); + autoCountdownTimer.start(intervalMs); } + function stopAutoCountdown() { - if (state.autoCountdownTimer) { - clearInterval(state.autoCountdownTimer); - state.autoCountdownTimer = null; - } state.nextAutoPingTime = null; + autoCountdownTimer.stop(); } -function updateApiCountdownStatus() { - if (!state.apiPostTime) { - return; - } - - const remainingMs = state.apiPostTime - Date.now(); - if (remainingMs <= 0) { - setStatus("Posting to API...", "text-sky-300"); - return; - } - - const remainingSec = Math.ceil(remainingMs / 1000); - setStatus(`Wait to post API (${remainingSec}s)`, "text-sky-300"); -} + function startApiCountdown(delayMs) { - // Stop any existing countdown - stopApiCountdown(); - - // Set the API post time state.apiPostTime = Date.now() + delayMs; - - // Update immediately - updateApiCountdownStatus(); - - // Update every second - state.apiCountdownTimer = setInterval(() => { - updateApiCountdownStatus(); - }, 1000); + apiCountdownTimer.start(delayMs); } + function stopApiCountdown() { - if (state.apiCountdownTimer) { - clearInterval(state.apiCountdownTimer); - state.apiCountdownTimer = null; - } state.apiPostTime = null; + apiCountdownTimer.stop(); } +// Cooldown management function isInCooldown() { return state.cooldownEndTime && Date.now() < state.cooldownEndTime; } + +function getRemainingCooldownSeconds() { + if (!isInCooldown()) return 0; + return Math.ceil((state.cooldownEndTime - Date.now()) / 1000); +} + function startCooldown() { state.cooldownEndTime = Date.now() + COOLDOWN_MS; updateControlsForCooldown(); - // Clear any existing cooldown update and schedule a new one if (state.cooldownUpdateTimer) { clearTimeout(state.cooldownUpdateTimer); } + state.cooldownUpdateTimer = setTimeout(() => { state.cooldownEndTime = null; updateControlsForCooldown(); }, COOLDOWN_MS); } + function updateControlsForCooldown() { const connected = !!state.connection; const inCooldown = isInCooldown(); sendPingBtn.disabled = !connected || inCooldown; autoToggleBtn.disabled = !connected || inCooldown; } + +// Timer cleanup +function cleanupAllTimers() { + debugLog("Cleaning up all timers"); + + if (state.meshMapperTimer) { + clearTimeout(state.meshMapperTimer); + state.meshMapperTimer = null; + } + + if (state.cooldownUpdateTimer) { + clearTimeout(state.cooldownUpdateTimer); + state.cooldownUpdateTimer = null; + } + + stopAutoCountdown(); + stopApiCountdown(); + state.cooldownEndTime = null; +} function enableControls(connected) { connectBtn.disabled = false; channelInfoEl.textContent = CHANNEL_NAME; @@ -505,35 +549,36 @@ function buildPayload(lat, lon) { } // ---- MeshMapper API ---- +/** + * Get the current device identifier for API calls + * @returns {string} Device name or default identifier + */ +function getDeviceIdentifier() { + const deviceText = deviceInfoEl?.textContent; + return (deviceText && deviceText !== "—") ? deviceText : MESHMAPPER_DEFAULT_WHO; +} + +/** + * Post wardrive ping data to MeshMapper API + * @param {number} lat - Latitude + * @param {number} lon - Longitude + */ async function postToMeshMapperAPI(lat, lon) { try { - - // Get current power setting - const power = getCurrentPowerSetting(); - const powerValue = power || "N/A"; - - // Use device name if available, otherwise use default - const deviceText = deviceInfoEl?.textContent; - const whoIdentifier = (deviceText && deviceText !== "—") ? deviceText : MESHMAPPER_DEFAULT_WHO; - - // Build API payload const payload = { key: MESHMAPPER_API_KEY, - lat: lat, - lon: lon, - who: whoIdentifier, - power: powerValue, + lat, + lon, + who: getDeviceIdentifier(), + power: getCurrentPowerSetting() || "N/A", test: 0 }; - debugLog(`Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${whoIdentifier}, power=${powerValue}`); + debugLog(`Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}`); - // POST to MeshMapper API const response = await fetch(MESHMAPPER_API_URL, { method: "POST", - headers: { - "Content-Type": "application/json" - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); @@ -548,68 +593,164 @@ async function postToMeshMapperAPI(lat, lon) { } } +/** + * Schedule MeshMapper API post and coverage map refresh after a ping + * @param {number} lat - Latitude + * @param {number} lon - Longitude + * @param {number} accuracy - GPS accuracy in meters + */ +function scheduleApiPostAndMapRefresh(lat, lon, accuracy) { + // Clear any existing timer + if (state.meshMapperTimer) { + debugLog("Clearing existing MeshMapper timer"); + clearTimeout(state.meshMapperTimer); + } + + debugLog(`Scheduling MeshMapper API post in ${MESHMAPPER_DELAY_MS}ms`); + + state.meshMapperTimer = setTimeout(async () => { + stopApiCountdown(); + setStatus("Posting to API...", STATUS_COLORS.info); + + try { + await postToMeshMapperAPI(lat, lon); + } catch (error) { + debugError("MeshMapper API post failed:", error); + } + + // Update map after API post + setTimeout(() => { + const shouldRefreshMap = accuracy && accuracy < GPS_ACCURACY_THRESHOLD_M; + + if (shouldRefreshMap) { + debugLog(`Refreshing coverage map (accuracy ${accuracy}m within threshold)`); + scheduleCoverageRefresh(lat, lon); + } else { + debugLog(`Skipping map refresh (accuracy ${accuracy}m exceeds threshold)`); + } + + // Update status based on current mode + if (state.connection) { + if (state.running) { + debugLog("Scheduling next auto ping"); + scheduleNextAutoPing(); + } else { + debugLog("Setting status to idle"); + setStatus("Idle", STATUS_COLORS.idle); + } + } + }, MAP_REFRESH_DELAY_MS); + + state.meshMapperTimer = null; + }, MESHMAPPER_DELAY_MS); +} + // ---- Ping ---- +/** + * Get GPS coordinates for ping operation + * @param {boolean} isAutoMode - Whether this is an auto ping + * @returns {Promise<{lat: number, lon: number, accuracy: number}|null>} GPS coordinates or null if unavailable + */ +async function getGpsCoordinatesForPing(isAutoMode) { + if (isAutoMode) { + // Auto mode: use GPS watch data only + if (!state.lastFix) { + debugWarn("Auto ping skipped: no GPS fix available yet"); + setStatus("Waiting for GPS fix...", STATUS_COLORS.warning); + return null; + } + debugLog(`Using GPS watch data: lat=${state.lastFix.lat.toFixed(5)}, lon=${state.lastFix.lon.toFixed(5)}`); + return { + lat: state.lastFix.lat, + lon: state.lastFix.lon, + accuracy: state.lastFix.accM + }; + } + + // Manual mode: check if cached data is recent enough + const intervalMs = getSelectedIntervalMs(); + const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; + const isCachedDataFresh = state.lastFix && (Date.now() - state.lastFix.tsMs) < maxAge; + + if (isCachedDataFresh) { + debugLog(`Using cached GPS data (age: ${Date.now() - state.lastFix.tsMs}ms)`); + return { + lat: state.lastFix.lat, + lon: state.lastFix.lon, + accuracy: state.lastFix.accM + }; + } + + // Get fresh GPS coordinates for manual ping + debugLog("Requesting fresh GPS position for manual ping"); + const pos = await getCurrentPosition(); + const coords = { + lat: pos.coords.latitude, + lon: pos.coords.longitude, + accuracy: pos.coords.accuracy + }; + debugLog(`Fresh GPS acquired: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); + + state.lastFix = { + lat: coords.lat, + lon: coords.lon, + accM: coords.accuracy, + tsMs: Date.now() + }; + updateGpsUi(); + + return coords; +} + +/** + * Log ping information to the UI + * @param {string} payload - The ping message + * @param {number} lat - Latitude + * @param {number} lon - Longitude + */ +function logPingToUI(payload, lat, lon) { + const nowStr = new Date().toLocaleString(); + + if (lastPingEl) { + lastPingEl.textContent = `${nowStr} — ${payload}`; + } + + if (sessionPingsEl) { + const line = `${nowStr} ${lat.toFixed(5)} ${lon.toFixed(5)}`; + const li = document.createElement('li'); + li.textContent = line; + sessionPingsEl.appendChild(li); + // Auto-scroll to bottom + sessionPingsEl.scrollTop = sessionPingsEl.scrollHeight; + } +} + +/** + * Send a wardrive ping with current GPS coordinates + * @param {boolean} manual - Whether this is a manual ping (true) or auto ping (false) + */ async function sendPing(manual = false) { debugLog(`sendPing called (manual=${manual})`); try { // Check cooldown only for manual pings if (manual && isInCooldown()) { - const remainingMs = state.cooldownEndTime - Date.now(); - const remainingSec = Math.ceil(remainingMs / 1000); + const remainingSec = getRemainingCooldownSeconds(); debugLog(`Manual ping blocked by cooldown (${remainingSec}s remaining)`); - setStatus(`Please wait ${remainingSec}s before sending another ping`, "text-amber-300"); + setStatus(`Please wait ${remainingSec}s before sending another ping`, STATUS_COLORS.warning); return; } // Stop the countdown timer when sending an auto ping to avoid status conflicts if (!manual && state.running) { stopAutoCountdown(); - setStatus("Sending auto ping...", "text-sky-300"); + setStatus("Sending auto ping...", STATUS_COLORS.info); } - let lat, lon, accuracy; - - // In auto mode, always use the most recent GPS coordinates from the watch - // In manual mode, get fresh GPS if needed - if (!manual && state.running) { - // Auto mode: use GPS watch data - if (!state.lastFix) { - // If no GPS fix yet in auto mode, skip this ping and wait for watch to acquire location - debugWarn("Auto ping skipped: no GPS fix available yet"); - setStatus("Waiting for GPS fix...", "text-amber-300"); - return; - } - debugLog(`Using GPS watch data for auto ping: lat=${state.lastFix.lat.toFixed(5)}, lon=${state.lastFix.lon.toFixed(5)}`); - lat = state.lastFix.lat; - lon = state.lastFix.lon; - accuracy = state.lastFix.accM; - } else { - // Manual mode: check if we have recent enough GPS data - const intervalMs = getSelectedIntervalMs(); - const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; // Allow buffer beyond interval - - if (state.lastFix && (Date.now() - state.lastFix.tsMs) < maxAge) { - debugLog(`Using cached GPS data (age: ${Date.now() - state.lastFix.tsMs}ms)`); - lat = state.lastFix.lat; - lon = state.lastFix.lon; - accuracy = state.lastFix.accM; - } else { - // Get fresh GPS coordinates for manual ping - debugLog("Requesting fresh GPS position for manual ping"); - const pos = await getCurrentPosition(); - lat = pos.coords.latitude; - lon = pos.coords.longitude; - accuracy = pos.coords.accuracy; - debugLog(`Fresh GPS acquired: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, accuracy=${accuracy}m`); - state.lastFix = { - lat, - lon, - accM: accuracy, - tsMs: Date.now(), - }; - updateGpsUi(); - } - } + // Get GPS coordinates + const coords = await getGpsCoordinatesForPing(!manual && state.running); + if (!coords) return; // GPS not available, message already shown + + const { lat, lon, accuracy } = coords; const payload = buildPayload(lat, lon); debugLog(`Sending ping to channel: "${payload}"`); @@ -623,80 +764,23 @@ async function sendPing(manual = false) { startCooldown(); // Update status after ping is sent - // Brief delay to show "Ping sent" status before moving to countdown - setStatus(manual ? "Ping sent" : "Auto ping sent", "text-emerald-300"); + setStatus(manual ? "Ping sent" : "Auto ping sent", STATUS_COLORS.success); + // Start API countdown after brief delay to show "Ping sent" message setTimeout(() => { if (state.connection) { - // Start countdown for API post startApiCountdown(MESHMAPPER_DELAY_MS); } }, STATUS_UPDATE_DELAY_MS); - // Schedule MeshMapper API call with 7-second delay (non-blocking) - // Clear any existing timer first - if (state.meshMapperTimer) { - debugLog("Clearing existing MeshMapper timer"); - clearTimeout(state.meshMapperTimer); - } - - debugLog(`Scheduling MeshMapper API post in ${MESHMAPPER_DELAY_MS}ms`); - state.meshMapperTimer = setTimeout(async () => { - // Capture accuracy in closure to ensure it's available in nested callback - const capturedAccuracy = accuracy; - - // Stop the API countdown since we're posting now - stopApiCountdown(); - setStatus("Posting to API...", "text-sky-300"); - - try { - await postToMeshMapperAPI(lat, lon); - } catch (error) { - debugError("MeshMapper API post failed:", error); - // Continue with map refresh and status update even if API fails - } - - // Update map after API post to ensure backend updated - setTimeout(() => { - if (capturedAccuracy && capturedAccuracy < GPS_ACCURACY_THRESHOLD_M) { - debugLog(`Refreshing coverage map (accuracy ${capturedAccuracy}m within threshold)`); - scheduleCoverageRefresh(lat, lon); - } else { - debugLog(`Skipping map refresh (accuracy ${capturedAccuracy}m exceeds threshold)`); - } - - // Set status to idle after map update - if (state.connection) { - // If in auto mode, schedule next ping. Otherwise, set to idle - if (state.running) { - // Schedule the next auto ping with countdown - debugLog("Scheduling next auto ping"); - scheduleNextAutoPing(); - } else { - debugLog("Setting status to idle"); - setStatus("Idle", "text-slate-300"); - } - } - }, MAP_REFRESH_DELAY_MS); - - state.meshMapperTimer = null; - }, MESHMAPPER_DELAY_MS); + // Schedule MeshMapper API post and map refresh + scheduleApiPostAndMapRefresh(lat, lon, accuracy); - const nowStr = new Date().toLocaleString(); - if (lastPingEl) lastPingEl.textContent = `${nowStr} — ${payload}`; - - // Session log - if (sessionPingsEl) { - const line = `${nowStr} ${lat.toFixed(5)} ${lon.toFixed(5)}`; - const li = document.createElement('li'); - li.textContent = line; - sessionPingsEl.appendChild(li); - // Auto-scroll to bottom when a new entry arrives - sessionPingsEl.scrollTop = sessionPingsEl.scrollHeight; - } + // Update UI with ping info + logPingToUI(payload, lat, lon); } catch (e) { debugError(`Ping operation failed: ${e.message}`, e); - setStatus(e.message || "Ping failed", "text-red-300"); + setStatus(e.message || "Ping failed", STATUS_COLORS.error); } } @@ -705,10 +789,9 @@ function stopAutoPing(stopGps = false) { debugLog(`stopAutoPing called (stopGps=${stopGps})`); // Check if we're in cooldown before stopping (unless stopGps is true for disconnect) if (!stopGps && isInCooldown()) { - const remainingMs = state.cooldownEndTime - Date.now(); - const remainingSec = Math.ceil(remainingMs / 1000); + const remainingSec = getRemainingCooldownSeconds(); debugLog(`Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); - setStatus(`Please wait ${remainingSec}s before toggling auto mode`, "text-amber-300"); + setStatus(`Please wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } @@ -760,10 +843,9 @@ function startAutoPing() { // Check if we're in cooldown if (isInCooldown()) { - const remainingMs = state.cooldownEndTime - Date.now(); - const remainingSec = Math.ceil(remainingMs / 1000); + const remainingSec = getRemainingCooldownSeconds(); debugLog(`Auto ping start blocked by cooldown (${remainingSec}s remaining)`); - setStatus(`Please wait ${remainingSec}s before toggling auto mode`, "text-amber-300"); + setStatus(`Please wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } @@ -800,7 +882,7 @@ async function connect() { return; } connectBtn.disabled = true; - setStatus("Connecting…", "text-sky-300"); + setStatus("Connecting…", STATUS_COLORS.info); try { debugLog("Opening BLE connection..."); @@ -810,7 +892,7 @@ async function connect() { conn.on("connected", async () => { debugLog("BLE connected event fired"); - setStatus("Connected", "text-emerald-300"); + setStatus("Connected", STATUS_COLORS.success); setConnectButton(true); connectBtn.disabled = false; const selfInfo = await conn.getSelfInfo(); @@ -828,13 +910,13 @@ async function connect() { await primeGpsOnce(); } catch (e) { debugError(`Channel setup failed: ${e.message}`, e); - setStatus(e.message || "Channel setup failed", "text-red-300"); + setStatus(e.message || "Channel setup failed", STATUS_COLORS.error); } }); conn.on("disconnected", () => { debugLog("BLE disconnected event fired"); - setStatus("Disconnected", "text-red-300"); + setStatus("Disconnected", STATUS_COLORS.error); setConnectButton(false); deviceInfoEl.textContent = "—"; state.connection = null; @@ -845,20 +927,8 @@ async function connect() { stopGeoWatch(); stopGpsAgeUpdater(); // Ensure age updater stops - // Clean up timers - if (state.meshMapperTimer) { - debugLog("Clearing MeshMapper timer on disconnect"); - clearTimeout(state.meshMapperTimer); - state.meshMapperTimer = null; - } - if (state.cooldownUpdateTimer) { - debugLog("Clearing cooldown timer on disconnect"); - clearTimeout(state.cooldownUpdateTimer); - state.cooldownUpdateTimer = null; - } - stopAutoCountdown(); - stopApiCountdown(); - state.cooldownEndTime = null; + // Clean up all timers + cleanupAllTimers(); state.lastFix = null; state.gpsState = "idle"; @@ -868,7 +938,7 @@ async function connect() { } catch (e) { debugError(`BLE connection failed: ${e.message}`, e); - setStatus("Failed to connect", "text-red-300"); + setStatus("Failed to connect", STATUS_COLORS.error); connectBtn.disabled = false; } } @@ -880,7 +950,7 @@ async function disconnect() { } connectBtn.disabled = true; - setStatus("Disconnecting...", "text-sky-300"); + setStatus("Disconnecting...", STATUS_COLORS.info); try { // WebBleConnection typically exposes one of these. @@ -898,7 +968,7 @@ async function disconnect() { } } catch (e) { debugError(`BLE disconnect failed: ${e.message}`, e); - setStatus(e.message || "Disconnect failed", "text-red-300"); + setStatus(e.message || "Disconnect failed", STATUS_COLORS.error); } finally { connectBtn.disabled = false; } @@ -912,7 +982,7 @@ document.addEventListener("visibilitychange", async () => { if (state.running) { debugLog("Stopping auto ping due to page hidden"); stopAutoPing(true); // Ignore cooldown check when page is hidden - setStatus("Lost focus, auto mode stopped", "text-amber-300"); + setStatus("Lost focus, auto mode stopped", STATUS_COLORS.warning); } else { debugLog("Releasing wake lock due to page hidden"); releaseWakeLock(); @@ -926,7 +996,7 @@ document.addEventListener("visibilitychange", async () => { // ---- Bind UI & init ---- export async function onLoad() { debugLog("wardrive.js onLoad() called - initializing"); - setStatus("Disconnected", "text-red-300"); + setStatus("Disconnected", STATUS_COLORS.error); enableControls(false); updateAutoButton(); @@ -939,7 +1009,7 @@ export async function onLoad() { } } catch (e) { debugError(`Connection button error: ${e.message}`, e); - setStatus(e.message || "Connection error", "text-red-300"); + setStatus(e.message || "Connection error", STATUS_COLORS.error); } }); sendPingBtn.addEventListener("click", () => { @@ -950,7 +1020,7 @@ export async function onLoad() { debugLog("Auto toggle button clicked"); if (state.running) { stopAutoPing(); - setStatus("Auto mode stopped", "text-slate-300"); + setStatus("Auto mode stopped", STATUS_COLORS.idle); } else { startAutoPing(); } From 9730e6dbf69f6be48cefea50651152a3658800a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:24:32 +0000 Subject: [PATCH 05/31] Fix code review issues: spacing and timer cleanup Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/content/wardrive.js b/content/wardrive.js index 7a3f321..e88f2ec 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -185,6 +185,7 @@ function stopApiCountdown() { state.apiPostTime = null; apiCountdownTimer.stop(); } + // Cooldown management function isInCooldown() { return state.cooldownEndTime && Date.now() < state.cooldownEndTime; @@ -230,10 +231,15 @@ function cleanupAllTimers() { state.cooldownUpdateTimer = null; } + // Clean up state timer references + state.autoCountdownTimer = null; + state.apiCountdownTimer = null; + stopAutoCountdown(); stopApiCountdown(); state.cooldownEndTime = null; } + function enableControls(connected) { connectBtn.disabled = false; channelInfoEl.textContent = CHANNEL_NAME; From 524f7c11794408f6145062e55546866df4542b0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:36:06 +0000 Subject: [PATCH 06/31] Initial plan From c0cdfbf9ee9bfad07ad1fbf31597ba369e352136 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:38:35 +0000 Subject: [PATCH 07/31] Fix manual ping timeout by using GPS watch data when available Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index e88f2ec..76a1aa2 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -673,11 +673,26 @@ async function getGpsCoordinatesForPing(isAutoMode) { }; } - // Manual mode: check if cached data is recent enough + // Manual mode: prefer GPS watch data if available and recent + // This prevents timeout issues when GPS watch is already running const intervalMs = getSelectedIntervalMs(); const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; + + // Check if GPS watch is active and providing recent data + const isGpsWatchActive = state.geoWatchId !== null; const isCachedDataFresh = state.lastFix && (Date.now() - state.lastFix.tsMs) < maxAge; + if (isGpsWatchActive && state.lastFix) { + // GPS watch is running - use its data regardless of age to avoid conflicts + const ageMs = Date.now() - state.lastFix.tsMs; + debugLog(`Using GPS watch data for manual ping (age: ${ageMs}ms, watch active)`); + return { + lat: state.lastFix.lat, + lon: state.lastFix.lon, + accuracy: state.lastFix.accM + }; + } + if (isCachedDataFresh) { debugLog(`Using cached GPS data (age: ${Date.now() - state.lastFix.tsMs}ms)`); return { @@ -687,7 +702,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { }; } - // Get fresh GPS coordinates for manual ping + // Get fresh GPS coordinates for manual ping (only when watch is not active) debugLog("Requesting fresh GPS position for manual ping"); const pos = await getCurrentPosition(); const coords = { From 913cd44d6dbec447295e7e5fb7a06d1965313805 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:39:48 +0000 Subject: [PATCH 08/31] Add maximum age check for GPS watch data in manual pings Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 76a1aa2..0ea4a59 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -677,20 +677,25 @@ async function getGpsCoordinatesForPing(isAutoMode) { // This prevents timeout issues when GPS watch is already running const intervalMs = getSelectedIntervalMs(); const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; + const maxWatchAge = 60000; // Maximum age for GPS watch data: 60 seconds // Check if GPS watch is active and providing recent data const isGpsWatchActive = state.geoWatchId !== null; const isCachedDataFresh = state.lastFix && (Date.now() - state.lastFix.tsMs) < maxAge; if (isGpsWatchActive && state.lastFix) { - // GPS watch is running - use its data regardless of age to avoid conflicts const ageMs = Date.now() - state.lastFix.tsMs; - debugLog(`Using GPS watch data for manual ping (age: ${ageMs}ms, watch active)`); - return { - lat: state.lastFix.lat, - lon: state.lastFix.lon, - accuracy: state.lastFix.accM - }; + // GPS watch is running - use its data if reasonably recent to avoid conflicts + if (ageMs < maxWatchAge) { + debugLog(`Using GPS watch data for manual ping (age: ${ageMs}ms, watch active)`); + return { + lat: state.lastFix.lat, + lon: state.lastFix.lon, + accuracy: state.lastFix.accM + }; + } else { + debugWarn(`GPS watch data too old (${ageMs}ms), requesting fresh position`); + } } if (isCachedDataFresh) { From 60e7f46e542a8cffc6cee3e403f5516e29fa3aaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:41:29 +0000 Subject: [PATCH 09/31] Refactor GPS coordinate logic with named constant and clearer flow Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 0ea4a59..4dfb168 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -37,6 +37,7 @@ const DEFAULT_INTERVAL_S = 30; // fallback if selector unavailab const PING_PREFIX = "@[MapperBot]"; const GPS_FRESHNESS_BUFFER_MS = 5000; // Buffer time for GPS freshness checks const GPS_ACCURACY_THRESHOLD_M = 100; // Maximum acceptable GPS accuracy in meters +const GPS_WATCH_MAX_AGE_MS = 60000; // Maximum age for GPS watch data in manual pings (60s) const MESHMAPPER_DELAY_MS = 7000; // Delay MeshMapper API call by 7 seconds const COOLDOWN_MS = 7000; // Cooldown period for manual ping and auto toggle const STATUS_UPDATE_DELAY_MS = 100; // Brief delay to ensure "Ping sent" status is visible @@ -675,39 +676,40 @@ async function getGpsCoordinatesForPing(isAutoMode) { // Manual mode: prefer GPS watch data if available and recent // This prevents timeout issues when GPS watch is already running - const intervalMs = getSelectedIntervalMs(); - const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; - const maxWatchAge = 60000; // Maximum age for GPS watch data: 60 seconds - - // Check if GPS watch is active and providing recent data const isGpsWatchActive = state.geoWatchId !== null; - const isCachedDataFresh = state.lastFix && (Date.now() - state.lastFix.tsMs) < maxAge; - if (isGpsWatchActive && state.lastFix) { + if (state.lastFix) { const ageMs = Date.now() - state.lastFix.tsMs; - // GPS watch is running - use its data if reasonably recent to avoid conflicts - if (ageMs < maxWatchAge) { + + // If GPS watch is running, use its data if recent (to avoid concurrent requests) + if (isGpsWatchActive && ageMs < GPS_WATCH_MAX_AGE_MS) { debugLog(`Using GPS watch data for manual ping (age: ${ageMs}ms, watch active)`); return { lat: state.lastFix.lat, lon: state.lastFix.lon, accuracy: state.lastFix.accM }; - } else { - debugWarn(`GPS watch data too old (${ageMs}ms), requesting fresh position`); } + + // If watch is not active, use cached data if fresh enough + if (!isGpsWatchActive) { + const intervalMs = getSelectedIntervalMs(); + const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; + if (ageMs < maxAge) { + debugLog(`Using cached GPS data (age: ${ageMs}ms, watch inactive)`); + return { + lat: state.lastFix.lat, + lon: state.lastFix.lon, + accuracy: state.lastFix.accM + }; + } + } + + // Data exists but is too old + debugLog(`GPS data too old (${ageMs}ms), requesting fresh position`); } - if (isCachedDataFresh) { - debugLog(`Using cached GPS data (age: ${Date.now() - state.lastFix.tsMs}ms)`); - return { - lat: state.lastFix.lat, - lon: state.lastFix.lon, - accuracy: state.lastFix.accM - }; - } - - // Get fresh GPS coordinates for manual ping (only when watch is not active) + // Get fresh GPS coordinates for manual ping debugLog("Requesting fresh GPS position for manual ping"); const pos = await getCurrentPosition(); const coords = { From b5ed6ef2adf48a3399d26bd7c2aa1e2db3538545 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:03:36 +0000 Subject: [PATCH 10/31] Initial plan From c5affe04040d2cd5383126e63e7e30311d6d0918 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:07:07 +0000 Subject: [PATCH 11/31] Add GPS status feedback for manual and auto ping operations - Manual ping now shows "GPS data too old, requesting fresh position" when requesting fresh GPS - Manual ping shows "Error: could not get fresh GPS location" on timeout - Auto ping validates GPS freshness before sending - Auto ping shows "GPS data old, trying to refresh position" when refreshing - Auto ping shows "GPS could not refresh position, skipping ping. Next attempt (Xs)" on failure - Auto ping schedules next attempt when GPS refresh fails Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 89 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 4dfb168..f71298b 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -660,13 +660,49 @@ function scheduleApiPostAndMapRefresh(lat, lon, accuracy) { */ async function getGpsCoordinatesForPing(isAutoMode) { if (isAutoMode) { - // Auto mode: use GPS watch data only + // Auto mode: validate GPS freshness before sending if (!state.lastFix) { debugWarn("Auto ping skipped: no GPS fix available yet"); setStatus("Waiting for GPS fix...", STATUS_COLORS.warning); return null; } - debugLog(`Using GPS watch data: lat=${state.lastFix.lat.toFixed(5)}, lon=${state.lastFix.lon.toFixed(5)}`); + + // Check if GPS data is too old for auto ping + const ageMs = Date.now() - state.lastFix.tsMs; + const intervalMs = getSelectedIntervalMs(); + const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; + + if (ageMs >= maxAge) { + debugLog(`GPS data too old for auto ping (${ageMs}ms), attempting to refresh`); + setStatus("GPS data old, trying to refresh position", STATUS_COLORS.warning); + + try { + const pos = await getCurrentPosition(); + const coords = { + lat: pos.coords.latitude, + lon: pos.coords.longitude, + accuracy: pos.coords.accuracy + }; + debugLog(`Fresh GPS acquired for auto ping: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); + + state.lastFix = { + lat: coords.lat, + lon: coords.lon, + accM: coords.accuracy, + tsMs: Date.now() + }; + updateGpsUi(); + + return coords; + } catch (e) { + debugError(`Could not refresh GPS position for auto ping: ${e.message}`, e); + const intervalSec = Math.ceil(intervalMs / 1000); + setStatus(`GPS could not refresh position, skipping ping. Next attempt (${intervalSec}s)`, STATUS_COLORS.error); + return null; + } + } + + debugLog(`Using GPS watch data: lat=${state.lastFix.lat.toFixed(5)}, lon=${state.lastFix.lon.toFixed(5)} (age: ${ageMs}ms)`); return { lat: state.lastFix.lat, lon: state.lastFix.lon, @@ -707,27 +743,33 @@ async function getGpsCoordinatesForPing(isAutoMode) { // Data exists but is too old debugLog(`GPS data too old (${ageMs}ms), requesting fresh position`); + setStatus("GPS data too old, requesting fresh position", STATUS_COLORS.warning); } // Get fresh GPS coordinates for manual ping debugLog("Requesting fresh GPS position for manual ping"); - const pos = await getCurrentPosition(); - const coords = { - lat: pos.coords.latitude, - lon: pos.coords.longitude, - accuracy: pos.coords.accuracy - }; - debugLog(`Fresh GPS acquired: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); - - state.lastFix = { - lat: coords.lat, - lon: coords.lon, - accM: coords.accuracy, - tsMs: Date.now() - }; - updateGpsUi(); - - return coords; + try { + const pos = await getCurrentPosition(); + const coords = { + lat: pos.coords.latitude, + lon: pos.coords.longitude, + accuracy: pos.coords.accuracy + }; + debugLog(`Fresh GPS acquired: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); + + state.lastFix = { + lat: coords.lat, + lon: coords.lon, + accM: coords.accuracy, + tsMs: Date.now() + }; + updateGpsUi(); + + return coords; + } catch (e) { + debugError(`Could not get fresh GPS location: ${e.message}`, e); + throw new Error("Error: could not get fresh GPS location"); + } } /** @@ -776,7 +818,14 @@ async function sendPing(manual = false) { // Get GPS coordinates const coords = await getGpsCoordinatesForPing(!manual && state.running); - if (!coords) return; // GPS not available, message already shown + if (!coords) { + // GPS not available, message already shown + // For auto mode, schedule next attempt + if (!manual && state.running) { + scheduleNextAutoPing(); + } + return; + } const { lat, lon, accuracy } = coords; From 5c4f254cd0c1d0f9e53a7640d312061a7ea4481a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:09:11 +0000 Subject: [PATCH 12/31] Refactor: Extract common GPS acquisition logic into helper function - Created acquireFreshGpsPosition() helper to eliminate code duplication - Reduces duplication between auto and manual GPS acquisition - Improves code maintainability and readability Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 61 ++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index f71298b..af6b8ef 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -653,6 +653,31 @@ function scheduleApiPostAndMapRefresh(lat, lon, accuracy) { } // ---- Ping ---- +/** + * Acquire fresh GPS coordinates and update state + * @returns {Promise<{lat: number, lon: number, accuracy: number}>} GPS coordinates + * @throws {Error} If GPS position cannot be acquired + */ +async function acquireFreshGpsPosition() { + const pos = await getCurrentPosition(); + const coords = { + lat: pos.coords.latitude, + lon: pos.coords.longitude, + accuracy: pos.coords.accuracy + }; + debugLog(`Fresh GPS acquired: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); + + state.lastFix = { + lat: coords.lat, + lon: coords.lon, + accM: coords.accuracy, + tsMs: Date.now() + }; + updateGpsUi(); + + return coords; +} + /** * Get GPS coordinates for ping operation * @param {boolean} isAutoMode - Whether this is an auto ping @@ -677,23 +702,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { setStatus("GPS data old, trying to refresh position", STATUS_COLORS.warning); try { - const pos = await getCurrentPosition(); - const coords = { - lat: pos.coords.latitude, - lon: pos.coords.longitude, - accuracy: pos.coords.accuracy - }; - debugLog(`Fresh GPS acquired for auto ping: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); - - state.lastFix = { - lat: coords.lat, - lon: coords.lon, - accM: coords.accuracy, - tsMs: Date.now() - }; - updateGpsUi(); - - return coords; + return await acquireFreshGpsPosition(); } catch (e) { debugError(`Could not refresh GPS position for auto ping: ${e.message}`, e); const intervalSec = Math.ceil(intervalMs / 1000); @@ -749,23 +758,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { // Get fresh GPS coordinates for manual ping debugLog("Requesting fresh GPS position for manual ping"); try { - const pos = await getCurrentPosition(); - const coords = { - lat: pos.coords.latitude, - lon: pos.coords.longitude, - accuracy: pos.coords.accuracy - }; - debugLog(`Fresh GPS acquired: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); - - state.lastFix = { - lat: coords.lat, - lon: coords.lon, - accM: coords.accuracy, - tsMs: Date.now() - }; - updateGpsUi(); - - return coords; + return await acquireFreshGpsPosition(); } catch (e) { debugError(`Could not get fresh GPS location: ${e.message}`, e); throw new Error("Error: could not get fresh GPS location"); From 2186af0af27be71944cfe29c2b1df285fb939302 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:11:59 +0000 Subject: [PATCH 13/31] Add GPS status feedback and freshness validation for ping operations Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 1 + 1 file changed, 1 insertion(+) diff --git a/content/wardrive.js b/content/wardrive.js index af6b8ef..3645853 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -761,6 +761,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { return await acquireFreshGpsPosition(); } catch (e) { debugError(`Could not get fresh GPS location: ${e.message}`, e); + // Note: "Error:" prefix is intentional per UX requirements for manual ping timeout throw new Error("Error: could not get fresh GPS location"); } } From decad0cd8275661ec306a5c03bf642b59f85baa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:06:43 +0000 Subject: [PATCH 14/31] Initial plan From 2723ae29c8d2586768f3e54d99a65abd0d539651 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:11:44 +0000 Subject: [PATCH 15/31] Implement GPS skip status with live countdown - Add skipReason state to track when ping is skipped - Update countdown timer to support color-coded messages - Show "Ping skipped, gps too old. Waiting for next auto ping (Xs)" with live countdown - Clear skip reason on auto ping start/stop and next ping attempt - Display skip message in warning color to indicate issue Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 64 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 3645853..3164cba 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -89,7 +89,8 @@ const state = { autoCountdownTimer: null, // Timer for auto-ping countdown display nextAutoPingTime: null, // Timestamp when next auto-ping will occur apiCountdownTimer: null, // Timer for API post countdown display - apiPostTime: null // Timestamp when API post will occur + apiPostTime: null, // Timestamp when API post will occur + skipReason: null // Reason for skipping a ping (e.g., "gps too old") }; // ---- UI helpers ---- @@ -125,14 +126,26 @@ function createCountdownTimer(getEndTime, getStatusMessage) { const remainingMs = this.endTime - Date.now(); if (remainingMs <= 0) { - const message = getStatusMessage(0); - if (message) setStatus(message, STATUS_COLORS.info); + const result = getStatusMessage(0); + if (!result) return; + // Handle both string and object format + if (typeof result === 'string') { + setStatus(result, STATUS_COLORS.info); + } else { + setStatus(result.message, result.color || STATUS_COLORS.info); + } return; } const remainingSec = Math.ceil(remainingMs / 1000); - const message = getStatusMessage(remainingSec); - if (message) setStatus(message, STATUS_COLORS.idle); + const result = getStatusMessage(remainingSec); + if (!result) return; + // Handle both string and object format + if (typeof result === 'string') { + setStatus(result, STATUS_COLORS.idle); + } else { + setStatus(result.message, result.color || STATUS_COLORS.idle); + } }, stop() { @@ -150,9 +163,20 @@ const autoCountdownTimer = createCountdownTimer( () => state.nextAutoPingTime, (remainingSec) => { if (!state.running) return null; - return remainingSec === 0 - ? "Sending auto ping..." - : `Waiting for next auto ping (${remainingSec}s)`; + if (remainingSec === 0) { + return { message: "Sending auto ping...", color: STATUS_COLORS.info }; + } + // If there's a skip reason, show it with the countdown in warning color + if (state.skipReason) { + return { + message: `Ping skipped, ${state.skipReason}. Waiting for next auto ping (${remainingSec}s)`, + color: STATUS_COLORS.warning + }; + } + return { + message: `Waiting for next auto ping (${remainingSec}s)`, + color: STATUS_COLORS.idle + }; } ); @@ -160,9 +184,13 @@ const autoCountdownTimer = createCountdownTimer( const apiCountdownTimer = createCountdownTimer( () => state.apiPostTime, (remainingSec) => { - return remainingSec === 0 - ? "Posting to API..." - : `Wait to post API (${remainingSec}s)`; + if (remainingSec === 0) { + return { message: "Posting to API...", color: STATUS_COLORS.info }; + } + return { + message: `Wait to post API (${remainingSec}s)`, + color: STATUS_COLORS.idle + }; } ); @@ -705,8 +733,8 @@ async function getGpsCoordinatesForPing(isAutoMode) { return await acquireFreshGpsPosition(); } catch (e) { debugError(`Could not refresh GPS position for auto ping: ${e.message}`, e); - const intervalSec = Math.ceil(intervalMs / 1000); - setStatus(`GPS could not refresh position, skipping ping. Next attempt (${intervalSec}s)`, STATUS_COLORS.error); + // Set skip reason so the countdown will show the appropriate message + state.skipReason = "gps too old"; return null; } } @@ -873,6 +901,9 @@ function stopAutoPing(stopGps = false) { } stopAutoCountdown(); + // Clear skip reason + state.skipReason = null; + // Only stop GPS watch when disconnecting or page hidden, not during normal stop if (stopGps) { stopGeoWatch(); @@ -892,12 +923,14 @@ function scheduleNextAutoPing() { const intervalMs = getSelectedIntervalMs(); debugLog(`Scheduling next auto ping in ${intervalMs}ms`); - // Start countdown immediately + // Start countdown immediately (skipReason may be set if ping was skipped) startAutoCountdown(intervalMs); // Schedule the next ping state.autoTimerId = setTimeout(() => { if (state.running) { + // Clear skip reason before next attempt + state.skipReason = null; debugLog("Auto ping timer fired, sending ping"); sendPing(false).catch(console.error); } @@ -928,6 +961,9 @@ function startAutoPing() { } stopAutoCountdown(); + // Clear any previous skip reason + state.skipReason = null; + // Start GPS watch for continuous updates debugLog("Starting GPS watch for auto mode"); startGeoWatch(); From de103c1dc047a8cdd46de7c1c1c54beec41bc456 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:12:39 +0000 Subject: [PATCH 16/31] Condense skip message for better UX Change from "Ping skipped, gps too old. Waiting for next auto ping (30s)" to "Skipped (gps too old), next ping (30s)" for more compact display Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/wardrive.js b/content/wardrive.js index 3164cba..f9278c6 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -169,7 +169,7 @@ const autoCountdownTimer = createCountdownTimer( // If there's a skip reason, show it with the countdown in warning color if (state.skipReason) { return { - message: `Ping skipped, ${state.skipReason}. Waiting for next auto ping (${remainingSec}s)`, + message: `Skipped (${state.skipReason}), next ping (${remainingSec}s)`, color: STATUS_COLORS.warning }; } From d65741a57d2fe84db70c8e896aff54da7113ef4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:15:56 +0000 Subject: [PATCH 17/31] Refactor countdown timer to reduce code duplication Extract applyCountdownStatus helper function to handle both string and object return formats, reducing duplication in update() method Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index f9278c6..eb56cd9 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -108,6 +108,16 @@ function setStatus(text, color = STATUS_COLORS.idle) { statusEl.className = `font-semibold ${color}`; } +// Helper function to apply status from countdown result +function applyCountdownStatus(result, defaultColor) { + if (!result) return; + if (typeof result === 'string') { + setStatus(result, defaultColor); + } else { + setStatus(result.message, result.color || defaultColor); + } +} + // Countdown timer management - generalized for reuse function createCountdownTimer(getEndTime, getStatusMessage) { return { @@ -126,26 +136,12 @@ function createCountdownTimer(getEndTime, getStatusMessage) { const remainingMs = this.endTime - Date.now(); if (remainingMs <= 0) { - const result = getStatusMessage(0); - if (!result) return; - // Handle both string and object format - if (typeof result === 'string') { - setStatus(result, STATUS_COLORS.info); - } else { - setStatus(result.message, result.color || STATUS_COLORS.info); - } + applyCountdownStatus(getStatusMessage(0), STATUS_COLORS.info); return; } const remainingSec = Math.ceil(remainingMs / 1000); - const result = getStatusMessage(remainingSec); - if (!result) return; - // Handle both string and object format - if (typeof result === 'string') { - setStatus(result, STATUS_COLORS.idle); - } else { - setStatus(result.message, result.color || STATUS_COLORS.idle); - } + applyCountdownStatus(getStatusMessage(remainingSec), STATUS_COLORS.idle); }, stop() { From 8543e558d39cab65baf5ee2bc1a5ec9cd6967835 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:17:54 +0000 Subject: [PATCH 18/31] Add JSDoc documentation for helper function Document applyCountdownStatus parameters and clarify skipReason is an internal value for security review Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index eb56cd9..0856cad 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -90,7 +90,7 @@ const state = { nextAutoPingTime: null, // Timestamp when next auto-ping will occur apiCountdownTimer: null, // Timer for API post countdown display apiPostTime: null, // Timestamp when API post will occur - skipReason: null // Reason for skipping a ping (e.g., "gps too old") + skipReason: null // Reason for skipping a ping - internal value only (e.g., "gps too old") }; // ---- UI helpers ---- @@ -108,7 +108,11 @@ function setStatus(text, color = STATUS_COLORS.idle) { statusEl.className = `font-semibold ${color}`; } -// Helper function to apply status from countdown result +/** + * Apply status message from countdown timer result + * @param {string|{message: string, color: string}|null} result - Status message (string) or object with message and optional color + * @param {string} defaultColor - Default color to use if result is a string or object without color + */ function applyCountdownStatus(result, defaultColor) { if (!result) return; if (typeof result === 'string') { From 94ba73c24e15b8425d5e9720118d56d724d85a56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:25:32 +0000 Subject: [PATCH 19/31] Initial plan From 2d0a7bf291126b68c7c652537e17b37f16a05391 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:29:05 +0000 Subject: [PATCH 20/31] Implement pause/resume for auto ping timer during manual pings Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 54 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 0856cad..209ce4a 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -90,7 +90,8 @@ const state = { nextAutoPingTime: null, // Timestamp when next auto-ping will occur apiCountdownTimer: null, // Timer for API post countdown display apiPostTime: null, // Timestamp when API post will occur - skipReason: null // Reason for skipping a ping - internal value only (e.g., "gps too old") + skipReason: null, // Reason for skipping a ping - internal value only (e.g., "gps too old") + pausedAutoTimerRemainingMs: null // Remaining time when auto ping timer was paused by manual ping }; // ---- UI helpers ---- @@ -205,6 +206,29 @@ function stopAutoCountdown() { autoCountdownTimer.stop(); } +function pauseAutoCountdown() { + // Calculate remaining time before pausing + if (state.nextAutoPingTime) { + const remainingMs = state.nextAutoPingTime - Date.now(); + state.pausedAutoTimerRemainingMs = Math.max(0, remainingMs); + debugLog(`Pausing auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); + } + // Stop the auto ping timer (but keep autoTimerId so we know auto mode is active) + autoCountdownTimer.stop(); + state.nextAutoPingTime = null; +} + +function resumeAutoCountdown() { + // Resume auto countdown from paused time + if (state.pausedAutoTimerRemainingMs !== null && state.pausedAutoTimerRemainingMs > 0) { + debugLog(`Resuming auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); + startAutoCountdown(state.pausedAutoTimerRemainingMs); + state.pausedAutoTimerRemainingMs = null; + return true; + } + return false; +} + function startApiCountdown(delayMs) { state.apiPostTime = Date.now() + delayMs; apiCountdownTimer.start(delayMs); @@ -267,6 +291,7 @@ function cleanupAllTimers() { stopAutoCountdown(); stopApiCountdown(); state.cooldownEndTime = null; + state.pausedAutoTimerRemainingMs = null; } function enableControls(connected) { @@ -667,8 +692,15 @@ function scheduleApiPostAndMapRefresh(lat, lon, accuracy) { // Update status based on current mode if (state.connection) { if (state.running) { - debugLog("Scheduling next auto ping"); - scheduleNextAutoPing(); + // Check if we should resume a paused auto countdown (manual ping during auto mode) + const resumed = resumeAutoCountdown(); + if (!resumed) { + // No paused timer to resume, schedule new auto ping (this was an auto ping) + debugLog("Scheduling next auto ping"); + scheduleNextAutoPing(); + } else { + debugLog("Resumed auto countdown after manual ping"); + } } else { debugLog("Setting status to idle"); setStatus("Idle", STATUS_COLORS.idle); @@ -832,10 +864,19 @@ async function sendPing(manual = false) { return; } - // Stop the countdown timer when sending an auto ping to avoid status conflicts - if (!manual && state.running) { + // Handle countdown timers based on ping type + if (manual && state.running) { + // Manual ping during auto mode: pause the auto countdown + debugLog("Manual ping during auto mode - pausing auto countdown"); + pauseAutoCountdown(); + setStatus("Sending manual ping...", STATUS_COLORS.info); + } else if (!manual && state.running) { + // Auto ping: stop the countdown timer to avoid status conflicts stopAutoCountdown(); setStatus("Sending auto ping...", STATUS_COLORS.info); + } else if (manual) { + // Manual ping when auto is not running + setStatus("Sending manual ping...", STATUS_COLORS.info); } // Get GPS coordinates @@ -901,8 +942,9 @@ function stopAutoPing(stopGps = false) { } stopAutoCountdown(); - // Clear skip reason + // Clear skip reason and paused timer state state.skipReason = null; + state.pausedAutoTimerRemainingMs = null; // Only stop GPS watch when disconnecting or page hidden, not during normal stop if (stopGps) { From 2bc8c8ade24fa71420c1ec1e8edc1c1fd3fa1f96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:32:49 +0000 Subject: [PATCH 21/31] Address code review: improve timer expiration handling Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- .gitignore | 3 +++ content/wardrive.js | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index cdb4317..c78af96 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ build/ # OS files .DS_Store Thumbs.db + +# Temporary files +tmp/ diff --git a/content/wardrive.js b/content/wardrive.js index 209ce4a..e14f4cb 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -210,8 +210,15 @@ function pauseAutoCountdown() { // Calculate remaining time before pausing if (state.nextAutoPingTime) { const remainingMs = state.nextAutoPingTime - Date.now(); - state.pausedAutoTimerRemainingMs = Math.max(0, remainingMs); - debugLog(`Pausing auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); + // Only pause if there's meaningful time remaining (more than 1 second) + // If timer already expired or about to expire, don't store it + if (remainingMs > 1000) { + state.pausedAutoTimerRemainingMs = remainingMs; + debugLog(`Pausing auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); + } else { + debugLog(`Auto countdown already expired or about to expire (${remainingMs}ms), not pausing`); + state.pausedAutoTimerRemainingMs = null; + } } // Stop the auto ping timer (but keep autoTimerId so we know auto mode is active) autoCountdownTimer.stop(); @@ -220,7 +227,7 @@ function pauseAutoCountdown() { function resumeAutoCountdown() { // Resume auto countdown from paused time - if (state.pausedAutoTimerRemainingMs !== null && state.pausedAutoTimerRemainingMs > 0) { + if (state.pausedAutoTimerRemainingMs !== null) { debugLog(`Resuming auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); startAutoCountdown(state.pausedAutoTimerRemainingMs); state.pausedAutoTimerRemainingMs = null; From f9c737c2335cb157b6b87bcb9276e5a294f34254 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:33:48 +0000 Subject: [PATCH 22/31] Add bounds checking for timer values to handle clock skew Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index e14f4cb..c2122b3 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -211,12 +211,13 @@ function pauseAutoCountdown() { if (state.nextAutoPingTime) { const remainingMs = state.nextAutoPingTime - Date.now(); // Only pause if there's meaningful time remaining (more than 1 second) - // If timer already expired or about to expire, don't store it - if (remainingMs > 1000) { + // and not unreasonably large (max 5 minutes to handle clock skew) + const maxReasonableMs = 5 * 60 * 1000; // 5 minutes + if (remainingMs > 1000 && remainingMs < maxReasonableMs) { state.pausedAutoTimerRemainingMs = remainingMs; debugLog(`Pausing auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); } else { - debugLog(`Auto countdown already expired or about to expire (${remainingMs}ms), not pausing`); + debugLog(`Auto countdown time out of reasonable range (${remainingMs}ms), not pausing`); state.pausedAutoTimerRemainingMs = null; } } @@ -228,10 +229,17 @@ function pauseAutoCountdown() { function resumeAutoCountdown() { // Resume auto countdown from paused time if (state.pausedAutoTimerRemainingMs !== null) { - debugLog(`Resuming auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); - startAutoCountdown(state.pausedAutoTimerRemainingMs); - state.pausedAutoTimerRemainingMs = null; - return true; + // Validate paused time is still reasonable before resuming + const maxReasonableMs = 5 * 60 * 1000; // 5 minutes + if (state.pausedAutoTimerRemainingMs > 1000 && state.pausedAutoTimerRemainingMs < maxReasonableMs) { + debugLog(`Resuming auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); + startAutoCountdown(state.pausedAutoTimerRemainingMs); + state.pausedAutoTimerRemainingMs = null; + return true; + } else { + debugLog(`Paused time out of reasonable range (${state.pausedAutoTimerRemainingMs}ms), not resuming`); + state.pausedAutoTimerRemainingMs = null; + } } return false; } From 92a2df5b34e0ab11c35c1b5f134f1b2af8cd13f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:35:16 +0000 Subject: [PATCH 23/31] Extract magic number to constant MAX_REASONABLE_TIMER_MS Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index c2122b3..faa82ff 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -42,6 +42,7 @@ const MESHMAPPER_DELAY_MS = 7000; // Delay MeshMapper API call by 7 const COOLDOWN_MS = 7000; // Cooldown period for manual ping and auto toggle const STATUS_UPDATE_DELAY_MS = 100; // Brief delay to ensure "Ping sent" status is visible const MAP_REFRESH_DELAY_MS = 1000; // Delay after API post to ensure backend updated +const MAX_REASONABLE_TIMER_MS = 5 * 60 * 1000; // Maximum reasonable timer value (5 minutes) to handle clock skew const WARDROVE_KEY = new Uint8Array([ 0x40, 0x76, 0xC3, 0x15, 0xC1, 0xEF, 0x38, 0x5F, 0xA9, 0x3F, 0x06, 0x60, 0x27, 0x32, 0x0F, 0xE5 @@ -211,9 +212,8 @@ function pauseAutoCountdown() { if (state.nextAutoPingTime) { const remainingMs = state.nextAutoPingTime - Date.now(); // Only pause if there's meaningful time remaining (more than 1 second) - // and not unreasonably large (max 5 minutes to handle clock skew) - const maxReasonableMs = 5 * 60 * 1000; // 5 minutes - if (remainingMs > 1000 && remainingMs < maxReasonableMs) { + // and not unreasonably large to handle clock skew + if (remainingMs > 1000 && remainingMs < MAX_REASONABLE_TIMER_MS) { state.pausedAutoTimerRemainingMs = remainingMs; debugLog(`Pausing auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); } else { @@ -230,8 +230,7 @@ function resumeAutoCountdown() { // Resume auto countdown from paused time if (state.pausedAutoTimerRemainingMs !== null) { // Validate paused time is still reasonable before resuming - const maxReasonableMs = 5 * 60 * 1000; // 5 minutes - if (state.pausedAutoTimerRemainingMs > 1000 && state.pausedAutoTimerRemainingMs < maxReasonableMs) { + if (state.pausedAutoTimerRemainingMs > 1000 && state.pausedAutoTimerRemainingMs < MAX_REASONABLE_TIMER_MS) { debugLog(`Resuming auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); startAutoCountdown(state.pausedAutoTimerRemainingMs); state.pausedAutoTimerRemainingMs = null; From e0abee72465f3a9f19c7658d7c9c2301a8f72e08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:36:33 +0000 Subject: [PATCH 24/31] Extract MIN_PAUSE_THRESHOLD_MS constant for consistency Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index faa82ff..67b16e9 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -42,6 +42,7 @@ const MESHMAPPER_DELAY_MS = 7000; // Delay MeshMapper API call by 7 const COOLDOWN_MS = 7000; // Cooldown period for manual ping and auto toggle const STATUS_UPDATE_DELAY_MS = 100; // Brief delay to ensure "Ping sent" status is visible const MAP_REFRESH_DELAY_MS = 1000; // Delay after API post to ensure backend updated +const MIN_PAUSE_THRESHOLD_MS = 1000; // Minimum timer value (1 second) to pause const MAX_REASONABLE_TIMER_MS = 5 * 60 * 1000; // Maximum reasonable timer value (5 minutes) to handle clock skew const WARDROVE_KEY = new Uint8Array([ 0x40, 0x76, 0xC3, 0x15, 0xC1, 0xEF, 0x38, 0x5F, @@ -211,9 +212,8 @@ function pauseAutoCountdown() { // Calculate remaining time before pausing if (state.nextAutoPingTime) { const remainingMs = state.nextAutoPingTime - Date.now(); - // Only pause if there's meaningful time remaining (more than 1 second) - // and not unreasonably large to handle clock skew - if (remainingMs > 1000 && remainingMs < MAX_REASONABLE_TIMER_MS) { + // Only pause if there's meaningful time remaining and not unreasonably large + if (remainingMs > MIN_PAUSE_THRESHOLD_MS && remainingMs < MAX_REASONABLE_TIMER_MS) { state.pausedAutoTimerRemainingMs = remainingMs; debugLog(`Pausing auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); } else { @@ -230,7 +230,7 @@ function resumeAutoCountdown() { // Resume auto countdown from paused time if (state.pausedAutoTimerRemainingMs !== null) { // Validate paused time is still reasonable before resuming - if (state.pausedAutoTimerRemainingMs > 1000 && state.pausedAutoTimerRemainingMs < MAX_REASONABLE_TIMER_MS) { + if (state.pausedAutoTimerRemainingMs > MIN_PAUSE_THRESHOLD_MS && state.pausedAutoTimerRemainingMs < MAX_REASONABLE_TIMER_MS) { debugLog(`Resuming auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); startAutoCountdown(state.pausedAutoTimerRemainingMs); state.pausedAutoTimerRemainingMs = null; From 739451040597275ba8b224bd99a93235f40e6d65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:01:52 +0000 Subject: [PATCH 25/31] Initial plan From 987bea28645a204955845d3e7a06700fc2765f93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:06:19 +0000 Subject: [PATCH 26/31] Add GPS geofence and distance-based ping filtering Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 206 +++++++++++++++++++++++++++++++++++++++++++- index.html | 4 + 2 files changed, 209 insertions(+), 1 deletion(-) diff --git a/content/wardrive.js b/content/wardrive.js index 67b16e9..cb6ce46 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -49,6 +49,14 @@ const WARDROVE_KEY = new Uint8Array([ 0xA9, 0x3F, 0x06, 0x60, 0x27, 0x32, 0x0F, 0xE5 ]); +// Ottawa Geofence Configuration +const OTTAWA_CENTER_LAT = 45.4215; // Parliament Hill latitude +const OTTAWA_CENTER_LON = -75.6972; // Parliament Hill longitude +const OTTAWA_GEOFENCE_RADIUS_M = 150000; // 150 km in meters + +// Distance-Based Ping Filtering +const MIN_PING_DISTANCE_M = 25; // Minimum distance (25m) between pings + // MeshMapper API Configuration const MESHMAPPER_API_URL = "https://yow.meshmapper.net/wardriving-api.php"; const MESHMAPPER_API_KEY = "59C7754DABDF5C11CA5F5D8368F89"; @@ -65,6 +73,7 @@ const autoToggleBtn = $("autoToggleBtn"); const lastPingEl = $("lastPing"); const gpsInfoEl = document.getElementById("gpsInfo"); const gpsAccEl = document.getElementById("gpsAcc"); +const distanceInfoEl = document.getElementById("distanceInfo"); // Distance from last ping const sessionPingsEl = document.getElementById("sessionPings"); // optional const coverageFrameEl = document.getElementById("coverageFrame"); setConnectButton(false); @@ -93,7 +102,9 @@ const state = { apiCountdownTimer: null, // Timer for API post countdown display apiPostTime: null, // Timestamp when API post will occur skipReason: null, // Reason for skipping a ping - internal value only (e.g., "gps too old") - pausedAutoTimerRemainingMs: null // Remaining time when auto ping timer was paused by manual ping + pausedAutoTimerRemainingMs: null, // Remaining time when auto ping timer was paused by manual ping + lastSuccessfulPingLocation: null, // { lat, lon } of the last successful ping (Mesh + API) + distanceUpdateTimer: null // Timer for updating distance display }; // ---- UI helpers ---- @@ -170,6 +181,18 @@ const autoCountdownTimer = createCountdownTimer( return { message: "Sending auto ping...", color: STATUS_COLORS.info }; } // If there's a skip reason, show it with the countdown in warning color + if (state.skipReason === "outside geofence") { + return { + message: `Ping skipped, outside of geo fenced region (${remainingSec}s)`, + color: STATUS_COLORS.warning + }; + } + if (state.skipReason === "too close") { + return { + message: `Ping skipping, too close to last ping, waiting for next ping (${remainingSec}s)`, + color: STATUS_COLORS.warning + }; + } if (state.skipReason) { return { message: `Skipped (${state.skipReason}), next ping (${remainingSec}s)`, @@ -416,6 +439,128 @@ async function releaseWakeLock() { } } +// ---- Geofence & Distance Validation ---- + +/** + * Calculate Haversine distance between two GPS coordinates + * @param {number} lat1 - First latitude + * @param {number} lon1 - First longitude + * @param {number} lat2 - Second latitude + * @param {number} lon2 - Second longitude + * @returns {number} Distance in meters + */ +function calculateHaversineDistance(lat1, lon1, lat2, lon2) { + debugLog(`Calculating Haversine distance: (${lat1.toFixed(5)}, ${lon1.toFixed(5)}) to (${lat2.toFixed(5)}, ${lon2.toFixed(5)})`); + + const R = 6371000; // Earth's radius in meters + const toRad = (deg) => (deg * Math.PI) / 180; + + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const distance = R * c; + + debugLog(`Haversine distance calculated: ${distance.toFixed(2)}m`); + return distance; +} + +/** + * Validate that GPS coordinates are within the Ottawa geofence + * @param {number} lat - Latitude to check + * @param {number} lon - Longitude to check + * @returns {boolean} True if within geofence, false otherwise + */ +function validateGeofence(lat, lon) { + debugLog(`Validating geofence for coordinates: (${lat.toFixed(5)}, ${lon.toFixed(5)})`); + debugLog(`Geofence center: (${OTTAWA_CENTER_LAT}, ${OTTAWA_CENTER_LON}), radius: ${OTTAWA_GEOFENCE_RADIUS_M}m`); + + const distance = calculateHaversineDistance(lat, lon, OTTAWA_CENTER_LAT, OTTAWA_CENTER_LON); + const isWithinGeofence = distance <= OTTAWA_GEOFENCE_RADIUS_M; + + debugLog(`Geofence validation: distance=${distance.toFixed(2)}m, within_geofence=${isWithinGeofence}`); + return isWithinGeofence; +} + +/** + * Validate that current GPS coordinates are at least 25m from last successful ping + * @param {number} lat - Current latitude + * @param {number} lon - Current longitude + * @returns {boolean} True if distance >= 25m or no previous ping, false otherwise + */ +function validateMinimumDistance(lat, lon) { + debugLog(`Validating minimum distance for coordinates: (${lat.toFixed(5)}, ${lon.toFixed(5)})`); + + if (!state.lastSuccessfulPingLocation) { + debugLog("No previous successful ping location, minimum distance check skipped"); + return true; + } + + const { lat: lastLat, lon: lastLon } = state.lastSuccessfulPingLocation; + debugLog(`Last successful ping location: (${lastLat.toFixed(5)}, ${lastLon.toFixed(5)})`); + + const distance = calculateHaversineDistance(lat, lon, lastLat, lastLon); + const isMinimumDistanceMet = distance >= MIN_PING_DISTANCE_M; + + debugLog(`Distance validation: distance=${distance.toFixed(2)}m, minimum_distance_met=${isMinimumDistanceMet} (threshold=${MIN_PING_DISTANCE_M}m)`); + return isMinimumDistanceMet; +} + +/** + * Calculate distance from last successful ping location (for UI display) + * @returns {number|null} Distance in meters, or null if no previous ping + */ +function getDistanceFromLastPing() { + if (!state.lastFix || !state.lastSuccessfulPingLocation) { + return null; + } + + const { lat, lon } = state.lastFix; + const { lat: lastLat, lon: lastLon } = state.lastSuccessfulPingLocation; + + return calculateHaversineDistance(lat, lon, lastLat, lastLon); +} + +/** + * Update the distance display in the UI + */ +function updateDistanceUi() { + if (!distanceInfoEl) return; + + const distance = getDistanceFromLastPing(); + + if (distance === null) { + distanceInfoEl.textContent = "-"; + } else { + distanceInfoEl.textContent = `${Math.round(distance)} m`; + } +} + +/** + * Start continuous distance display updates + */ +function startDistanceUpdater() { + if (state.distanceUpdateTimer) return; + state.distanceUpdateTimer = setInterval(() => { + updateDistanceUi(); + }, 1000); // Update every second +} + +/** + * Stop distance display updates + */ +function stopDistanceUpdater() { + if (state.distanceUpdateTimer) { + clearInterval(state.distanceUpdateTimer); + state.distanceUpdateTimer = null; + } +} + // ---- Geolocation ---- async function getCurrentPosition() { return new Promise((resolve, reject) => { @@ -489,6 +634,7 @@ function startGeoWatch() { state.gpsState = "acquiring"; updateGpsUi(); startGpsAgeUpdater(); // Start the age counter + startDistanceUpdater(); // Start the distance updater state.geoWatchId = navigator.geolocation.watchPosition( (pos) => { @@ -501,6 +647,7 @@ function startGeoWatch() { }; state.gpsState = "acquired"; updateGpsUi(); + updateDistanceUi(); }, (err) => { debugError(`GPS watch error: ${err.code} - ${err.message}`); @@ -524,6 +671,7 @@ function stopGeoWatch() { navigator.geolocation.clearWatch(state.geoWatchId); state.geoWatchId = null; stopGpsAgeUpdater(); // Stop the age counter + stopDistanceUpdater(); // Stop the distance updater } async function primeGpsOnce() { debugLog("Priming GPS with initial position request"); @@ -906,6 +1054,49 @@ async function sendPing(manual = false) { const { lat, lon, accuracy } = coords; + // VALIDATION 1: Geofence check (FIRST - must be within Ottawa 150km) + debugLog("Starting geofence validation"); + if (!validateGeofence(lat, lon)) { + debugLog("Ping blocked: outside geofence"); + + // Set skip reason for auto mode countdown display + state.skipReason = "outside geofence"; + + if (manual) { + // Manual ping: show skip message that persists + setStatus("Ping skipped, outside of geo fenced region", STATUS_COLORS.warning); + } else if (state.running) { + // Auto ping: schedule next ping and show countdown with skip message + scheduleNextAutoPing(); + } + + return; + } + debugLog("Geofence validation passed"); + + // VALIDATION 2: Distance check (SECOND - must be ≥ 25m from last successful ping) + debugLog("Starting distance validation"); + if (!validateMinimumDistance(lat, lon)) { + debugLog("Ping blocked: too close to last ping"); + + // Set skip reason for auto mode countdown display + state.skipReason = "too close"; + + if (manual) { + // Manual ping: show skip message that persists + setStatus("Ping skipped, too close to last ping", STATUS_COLORS.warning); + } else if (state.running) { + // Auto ping: schedule next ping and show countdown with skip message + scheduleNextAutoPing(); + } + + return; + } + debugLog("Distance validation passed"); + + // Both validations passed - execute ping operation (Mesh + API) + debugLog("All validations passed, executing ping operation"); + const payload = buildPayload(lat, lon); debugLog(`Sending ping to channel: "${payload}"`); @@ -913,6 +1104,13 @@ async function sendPing(manual = false) { await state.connection.sendChannelTextMessage(ch.channelIdx, payload); debugLog(`Ping sent successfully to channel ${ch.channelIdx}`); + // Ping operation succeeded - update last successful ping location + state.lastSuccessfulPingLocation = { lat, lon }; + debugLog(`Updated last successful ping location: (${lat.toFixed(5)}, ${lon.toFixed(5)})`); + + // Clear skip reason on successful ping + state.skipReason = null; + // Start cooldown period after successful ping debugLog(`Starting ${COOLDOWN_MS}ms cooldown`); startCooldown(); @@ -932,6 +1130,9 @@ async function sendPing(manual = false) { // Update UI with ping info logPingToUI(payload, lat, lon); + + // Update distance display immediately after successful ping + updateDistanceUi(); } catch (e) { debugError(`Ping operation failed: ${e.message}`, e); setStatus(e.message || "Ping failed", STATUS_COLORS.error); @@ -1089,13 +1290,16 @@ async function connect() { updateAutoButton(); stopGeoWatch(); stopGpsAgeUpdater(); // Ensure age updater stops + stopDistanceUpdater(); // Ensure distance updater stops // Clean up all timers cleanupAllTimers(); state.lastFix = null; + state.lastSuccessfulPingLocation = null; state.gpsState = "idle"; updateGpsUi(); + updateDistanceUi(); debugLog("Disconnect cleanup complete"); }); diff --git a/index.html b/index.html index 2324745..1c7808e 100644 --- a/index.html +++ b/index.html @@ -67,6 +67,10 @@

Accuracy: - + + ΔDist: + - + From d50e8d2f1b7eabedcda122e5ebb5939b7affa27c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:09:38 +0000 Subject: [PATCH 27/31] Optimize distance updater for better battery life Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index cb6ce46..77dd698 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -548,7 +548,7 @@ function startDistanceUpdater() { if (state.distanceUpdateTimer) return; state.distanceUpdateTimer = setInterval(() => { updateDistanceUi(); - }, 1000); // Update every second + }, 3000); // Update every 3 seconds as fallback (main updates happen on GPS position changes) } /** @@ -647,7 +647,7 @@ function startGeoWatch() { }; state.gpsState = "acquired"; updateGpsUi(); - updateDistanceUi(); + updateDistanceUi(); // Update distance when GPS position changes }, (err) => { debugError(`GPS watch error: ${err.code} - ${err.message}`); From 15faa35b7a16f0e0b9fbc3b7593f2197a0b4b9c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:26:15 +0000 Subject: [PATCH 28/31] Update auto geofence skip message format Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/wardrive.js b/content/wardrive.js index 77dd698..3e23b20 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -183,7 +183,7 @@ const autoCountdownTimer = createCountdownTimer( // If there's a skip reason, show it with the countdown in warning color if (state.skipReason === "outside geofence") { return { - message: `Ping skipped, outside of geo fenced region (${remainingSec}s)`, + message: `Ping skipped, outside of geo fenced region, waiting for next ping (${remainingSec}s)`, color: STATUS_COLORS.warning }; } From 14fd020a2c32759a936b343dd37924d21b60e3d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:27:49 +0000 Subject: [PATCH 29/31] =?UTF-8?q?Change=20field=20name=20from=20=CE=94Dist?= =?UTF-8?q?=20to=20distance=20from=20last=20ping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 1c7808e..d61efca 100644 --- a/index.html +++ b/index.html @@ -68,7 +68,7 @@

- - ΔDist: + distance from last ping: - From 1d2ebe184f2e7d04fb5c1bcd96f4ef1363dd95d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:31:41 +0000 Subject: [PATCH 30/31] Capitalize D in Distance from last ping field Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index d61efca..d7aef1c 100644 --- a/index.html +++ b/index.html @@ -68,7 +68,7 @@

- - distance from last ping: + Distance from last ping: - From 51ddfe7f51895581fc868f2b82f723eb5856ec55 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Thu, 18 Dec 2025 15:55:32 -0500 Subject: [PATCH 31/31] Update Android and iOS setup instructions for joining the wardriving channel --- README.md | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9ab08da..8815eb4 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,33 @@ A browser-based Progressive Web App for wardriving with MeshCore devices. Connec - Make sure you have the **wardriving channel** set on your companion. - Take a **backup of your companion** (this webapp is beyond experimental). -### Android (recommended) -1. Disconnect the **MeshCore** app and close it -2. Open **Google Chrome** -3. Go to https://wardrive.ottawamesh.ca/ -4. **Connect** your device -5. Pick **interval** and **power** -6. Send a ping or start **auto ping** -7. Move around and watch it track +### Android +1. Open the **MeshCore** app and add the Public Hashtag channel #wardriving: + - Click the three dots (top-right) → **Add Channel** + - Select **Join a hashtag channel** + - Type **#wardriving** → **Join channel** + - Click **Continue to channel** +2. Disconnect the **MeshCore** app and close it +3. Open **Google Chrome** +4. Go to https://wardrive.ottawamesh.ca/ +5. **Connect** your device +6. Pick **interval** and **power** +7. Send a ping or start **auto ping** +8. Move around and watch it track ### iOS -1. Disconnect the **MeshCore** app and close it -2. Install **Bluefy** (Web BLE browser): https://apps.apple.com/us/app/bluefy-web-ble-browser/id1492822055 -3. Open https://wardrive.ottawamesh.ca/ in **Bluefy** -4. **Connect** your device -5. Pick **interval** and **power** -6. Send a ping or start **auto ping** -7. Move around and watch it track +1. Open the **MeshCore** app and add the Public Hashtag channel #wardriving: + - Click the three dots (top-right) → **Add Channel** + - Select **Join a hashtag channel** + - Type **#wardriving** → **Join channel** + - Click **Continue to channel** +2. Disconnect the **MeshCore** app and close it +3. Install **Bluefy** (Web BLE browser): https://apps.apple.com/us/app/bluefy-web-ble-browser/id1492822055 +4. Open https://wardrive.ottawamesh.ca/ in **Bluefy** +5. **Connect** your device +6. Pick **interval** and **power** +7. Send a ping or start **auto ping** +8. Move around and watch it track > ⚠️ **Note (iOS)**: You must use **Bluefy**. Other iOS browsers (including Safari) do not support Web Bluetooth (BLE).