diff --git a/content/wardrive.js b/content/wardrive.js index dd8ac72..7d242a6 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -28,6 +28,24 @@ function debugWarn(message, ...args) { function debugError(message, ...args) { if (DEBUG_ENABLED) { console.error(`[DEBUG] ${message}`, ...args); + + // Also add to Error Log UI (use try-catch to prevent recursive errors) + try { + // Extract source tag if present (e.g., "[BLE]" from "[BLE] Connection failed") + const tagMatch = message.match(/^\[([^\]]+)\]/); + const source = tagMatch ? tagMatch[1] : null; + + // Remove tag from message if present + const cleanMessage = tagMatch ? message.replace(/^\[[^\]]+\]\s*/, '') : message; + + // Only add to Error Log if the UI is initialized + if (typeof addErrorLogEntry === 'function') { + addErrorLogEntry(cleanMessage, source); + } + } catch (e) { + // Silently fail to prevent recursive errors + console.error('Failed to add error to Error Log UI:', e); + } } } @@ -57,11 +75,11 @@ let WARDRIVING_CHANNEL_KEY = null; try { WARDRIVING_CHANNEL_KEY = await deriveChannelKey(CHANNEL_NAME); WARDRIVING_CHANNEL_HASH = await computeChannelHash(WARDRIVING_CHANNEL_KEY); - debugLog(`Wardriving channel hash pre-computed at startup: 0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')}`); - debugLog(`Wardriving channel key cached for message decryption (${WARDRIVING_CHANNEL_KEY.length} bytes)`); + debugLog(`[INIT] Wardriving channel hash pre-computed at startup: 0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')}`); + debugLog(`[INIT] Wardriving channel key cached for message decryption (${WARDRIVING_CHANNEL_KEY.length} bytes)`); } catch (error) { - debugError(`CRITICAL: Failed to pre-compute channel hash/key: ${error.message}`); - debugError(`Repeater echo tracking will be disabled. Please reload the page.`); + debugError(`[INIT] CRITICAL: Failed to pre-compute channel hash/key: ${error.message}`); + debugError(`[INIT] Repeater echo tracking will be disabled. Please reload the page.`); // Channel hash and key remain null, which will be checked before starting tracking } })(); @@ -149,6 +167,16 @@ const rxLogLastRepeater = $("rxLogLastRepeater"); const rxLogEntries = $("rxLogEntries"); const rxLogExpandArrow = $("rxLogExpandArrow"); +// Error Log selectors +const errorLogSummaryBar = $("errorLogSummaryBar"); +const errorLogBottomSheet = $("errorLogBottomSheet"); +const errorLogScrollContainer = $("errorLogScrollContainer"); +const errorLogCount = $("errorLogCount"); +const errorLogLastTime = $("errorLogLastTime"); +const errorLogLastError = $("errorLogLastError"); +const errorLogEntries = $("errorLogEntries"); +const errorLogExpandArrow = $("errorLogExpandArrow"); + // Session log state const sessionLogState = { entries: [], // Array of parsed log entries @@ -164,6 +192,14 @@ const rxLogState = { maxEntries: 100 // Limit to prevent memory issues }; +// Error log state +const errorLogState = { + entries: [], // Array of error log entries + isExpanded: false, + autoScroll: true, + maxEntries: 50 // Limit to prevent memory issues +}; + // ---- State ---- const state = { connection: null, @@ -242,7 +278,7 @@ function setStatus(text, color = STATUS_COLORS.idle, immediate = false) { // This prevents countdown timer updates from being delayed unnecessarily // Example: If status is already "Waiting (10s)", the next "Waiting (9s)" won't be delayed if (text === statusMessageState.currentText && color === statusMessageState.currentColor) { - debugLog(`Status update (same message): "${text}"`); + debugLog(`[UI] Status update (same message): "${text}"`); statusMessageState.lastSetTime = now; return; } @@ -261,7 +297,7 @@ function setStatus(text, color = STATUS_COLORS.idle, immediate = false) { // Minimum visibility time has not passed, queue the message const delayNeeded = MIN_STATUS_VISIBILITY_MS - timeSinceLastSet; - debugLog(`Status queued (${delayNeeded}ms delay): "${text}" (current: "${statusMessageState.currentText}")`); + debugLog(`[UI] Status queued (${delayNeeded}ms delay): "${text}" (current: "${statusMessageState.currentText}")`); // Store pending message statusMessageState.pendingMessage = { text, color }; @@ -293,7 +329,7 @@ function applyStatusImmediately(text, color) { statusMessageState.lastSetTime = Date.now(); statusMessageState.currentText = text; statusMessageState.currentColor = color; - debugLog(`Status applied: "${text}"`); + debugLog(`[UI] Status applied: "${text}"`); } /** @@ -424,9 +460,9 @@ function pauseAutoCountdown() { // 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`); + debugLog(`[TIMER] Pausing auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); } else { - debugLog(`Auto countdown time out of reasonable range (${remainingMs}ms), not pausing`); + debugLog(`[TIMER] Auto countdown time out of reasonable range (${remainingMs}ms), not pausing`); state.pausedAutoTimerRemainingMs = null; } } @@ -440,12 +476,12 @@ function resumeAutoCountdown() { if (state.pausedAutoTimerRemainingMs !== null) { // Validate paused time is still reasonable before resuming if (state.pausedAutoTimerRemainingMs > MIN_PAUSE_THRESHOLD_MS && state.pausedAutoTimerRemainingMs < MAX_REASONABLE_TIMER_MS) { - debugLog(`Resuming auto countdown with ${state.pausedAutoTimerRemainingMs}ms remaining`); + debugLog(`[TIMER] 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`); + debugLog(`[TIMER] Paused time out of reasonable range (${state.pausedAutoTimerRemainingMs}ms), not resuming`); state.pausedAutoTimerRemainingMs = null; } } @@ -465,23 +501,23 @@ function resumeAutoCountdown() { */ function handleManualPingBlockedDuringAutoMode() { if (state.running) { - debugLog("Manual ping blocked during auto mode - resuming auto countdown"); + debugLog("[AUTO] Manual ping blocked during auto mode - resuming auto countdown"); const resumed = resumeAutoCountdown(); if (!resumed) { - debugLog("No paused countdown to resume, scheduling new auto ping"); + debugLog("[AUTO] No paused countdown to resume, scheduling new auto ping"); scheduleNextAutoPing(); } } } function startRxListeningCountdown(delayMs) { - debugLog(`Starting RX listening countdown: ${delayMs}ms`); + debugLog(`[TIMER] Starting RX listening countdown: ${delayMs}ms`); state.rxListeningEndTime = Date.now() + delayMs; rxListeningCountdownTimer.start(delayMs); } function stopRxListeningCountdown() { - debugLog(`Stopping RX listening countdown`); + debugLog(`[TIMER] Stopping RX listening countdown`); state.rxListeningEndTime = null; rxListeningCountdownTimer.stop(); } @@ -513,7 +549,7 @@ function startCooldown() { function updateControlsForCooldown() { const connected = !!state.connection; const inCooldown = isInCooldown(); - debugLog(`updateControlsForCooldown: connected=${connected}, inCooldown=${inCooldown}, pingInProgress=${state.pingInProgress}`); + debugLog(`[UI] updateControlsForCooldown: connected=${connected}, inCooldown=${inCooldown}, pingInProgress=${state.pingInProgress}`); sendPingBtn.disabled = !connected || inCooldown || state.pingInProgress; autoToggleBtn.disabled = !connected || inCooldown || state.pingInProgress; } @@ -525,12 +561,12 @@ function updateControlsForCooldown() { function unlockPingControls(reason) { state.pingInProgress = false; updateControlsForCooldown(); - debugLog(`Ping controls unlocked (pingInProgress=false) ${reason}`); + debugLog(`[UI] Ping controls unlocked (pingInProgress=false) ${reason}`); } // Timer cleanup function cleanupAllTimers() { - debugLog("Cleaning up all timers"); + debugLog("[TIMER] Cleaning up all timers"); if (state.meshMapperTimer) { clearTimeout(state.meshMapperTimer); @@ -580,7 +616,7 @@ function cleanupAllTimers() { } } state.rxBatchBuffer.clear(); - debugLog("RX batch buffer cleared"); + debugLog("[RX BATCH] RX batch buffer cleared"); } } @@ -617,7 +653,7 @@ function scheduleCoverageRefresh(lat, lon, delayMs = 0) { coverageRefreshTimer = setTimeout(() => { const url = buildCoverageEmbedUrl(lat, lon); - debugLog("Coverage iframe URL:", url); + debugLog("[UI] Coverage iframe URL:", url); coverageFrameEl.src = url; }, delayMs); } @@ -663,7 +699,7 @@ function setConnStatus(text, color) { if (!connectionStatusEl) return; - debugLog(`Connection status: "${text}"`); + debugLog(`[UI] Connection status: "${text}"`); connectionStatusEl.textContent = text; connectionStatusEl.className = `font-medium ${color}`; @@ -695,7 +731,7 @@ function setDynamicStatus(text, color = STATUS_COLORS.idle, immediate = false) { // Block connection words from dynamic bar const connectionWords = ['Connected', 'Connecting', 'Disconnecting', 'Disconnected']; if (connectionWords.includes(text)) { - debugWarn(`Attempted to show connection word "${text}" in dynamic status bar - blocked, showing em dash instead`); + debugWarn(`[UI] Attempted to show connection word "${text}" in dynamic status bar - blocked, showing em dash instead`); text = '—'; color = STATUS_COLORS.idle; } @@ -708,48 +744,48 @@ function setDynamicStatus(text, color = STATUS_COLORS.idle, immediate = false) { // ---- Wake Lock helpers ---- async function acquireWakeLock() { - debugLog("Attempting to acquire wake lock"); + debugLog("[WAKE LOCK] Attempting to acquire wake lock"); if (navigator.bluetooth && typeof navigator.bluetooth.setScreenDimEnabled === "function") { try { navigator.bluetooth.setScreenDimEnabled(true); state.bluefyLockEnabled = true; - debugLog("Bluefy screen-dim prevention enabled"); + debugLog("[WAKE LOCK] Bluefy screen-dim prevention enabled"); return; } catch (e) { - debugWarn("Bluefy setScreenDimEnabled failed:", e); + debugWarn("[WAKE LOCK] Bluefy setScreenDimEnabled failed:", e); } } try { if ("wakeLock" in navigator && typeof navigator.wakeLock.request === "function") { state.wakeLock = await navigator.wakeLock.request("screen"); - debugLog("Wake lock acquired successfully"); - state.wakeLock.addEventListener?.("release", () => debugLog("Wake lock released")); + debugLog("[WAKE LOCK] Wake lock acquired successfully"); + state.wakeLock.addEventListener?.("release", () => debugLog("[WAKE LOCK] Wake lock released")); } else { - debugLog("Wake Lock API not supported on this device"); + debugLog("[WAKE LOCK] Wake Lock API not supported on this device"); } } catch (err) { - debugError(`Could not obtain wake lock: ${err.name}, ${err.message}`); + debugError(`[WAKE LOCK] Could not obtain wake lock: ${err.name}, ${err.message}`); } } async function releaseWakeLock() { - debugLog("Attempting to release wake lock"); + debugLog("[WAKE LOCK] Attempting to release wake lock"); if (state.bluefyLockEnabled && navigator.bluetooth && typeof navigator.bluetooth.setScreenDimEnabled === "function") { try { navigator.bluetooth.setScreenDimEnabled(false); state.bluefyLockEnabled = false; - debugLog("Bluefy screen-dim prevention disabled"); + debugLog("[WAKE LOCK] Bluefy screen-dim prevention disabled"); } catch (e) { - debugWarn("Bluefy setScreenDimEnabled(false) failed:", e); + debugWarn("[WAKE LOCK] Bluefy setScreenDimEnabled(false) failed:", e); } } try { if (state.wakeLock) { await state.wakeLock.release?.(); state.wakeLock = null; - debugLog("Wake lock released successfully"); + debugLog("[WAKE LOCK] Wake lock released successfully"); } } catch (e) { - debugWarn("Error releasing wake lock:", e); + debugWarn("[WAKE LOCK] Error releasing wake lock:", e); state.wakeLock = null; } } @@ -765,7 +801,7 @@ async function releaseWakeLock() { * @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)})`); + debugLog(`[GEOFENCE] 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; @@ -781,7 +817,7 @@ function calculateHaversineDistance(lat1, lon1, lat2, lon2) { const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; - debugLog(`Haversine distance calculated: ${distance.toFixed(2)}m`); + debugLog(`[GEOFENCE] Haversine distance calculated: ${distance.toFixed(2)}m`); return distance; } @@ -792,13 +828,13 @@ function calculateHaversineDistance(lat1, lon1, lat2, lon2) { * @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`); + debugLog(`[GEOFENCE] Validating geofence for coordinates: (${lat.toFixed(5)}, ${lon.toFixed(5)})`); + debugLog(`[GEOFENCE] 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}`); + debugLog(`[GEOFENCE] Geofence validation: distance=${distance.toFixed(2)}m, within_geofence=${isWithinGeofence}`); return isWithinGeofence; } @@ -809,20 +845,20 @@ function validateGeofence(lat, lon) { * @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)})`); + debugLog(`[GEOFENCE] Validating minimum distance for coordinates: (${lat.toFixed(5)}, ${lon.toFixed(5)})`); if (!state.lastSuccessfulPingLocation) { - debugLog("No previous successful ping location, minimum distance check skipped"); + debugLog("[GEOFENCE] 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)})`); + debugLog(`[GEOFENCE] 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)`); + debugLog(`[GEOFENCE] Distance validation: distance=${distance.toFixed(2)}m, minimum_distance_met=${isMinimumDistanceMet} (threshold=${MIN_PING_DISTANCE_M}m)`); return isMinimumDistanceMet; } @@ -920,22 +956,22 @@ function stopGpsAgeUpdater() { } function startGeoWatch() { if (state.geoWatchId) { - debugLog("GPS watch already running, skipping start"); + debugLog("[GPS] GPS watch already running, skipping start"); return; } if (!("geolocation" in navigator)) { - debugError("Geolocation not available in navigator"); + debugError("[GPS] Geolocation not available in navigator"); return; } - debugLog("Starting GPS watch"); + debugLog("[GPS] 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`); + debugLog(`[GPS] 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, @@ -947,7 +983,7 @@ function startGeoWatch() { updateDistanceUi(); // Update distance when GPS position changes }, (err) => { - debugError(`GPS watch error: ${err.code} - ${err.message}`); + debugError(`[GPS] GPS watch error: ${err.code} - ${err.message}`); state.gpsState = "error"; // Display GPS error in Dynamic Status Bar setDynamicStatus("GPS error - check permissions", STATUS_COLORS.error); @@ -963,16 +999,16 @@ function startGeoWatch() { } function stopGeoWatch() { if (!state.geoWatchId) { - debugLog("No GPS watch to stop"); + debugLog("[GPS] No GPS watch to stop"); return; } - debugLog("Stopping GPS watch"); + debugLog("[GPS] 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"); + debugLog("[GPS] Priming GPS with initial position request"); // Start continuous watch so the UI keeps updating startGeoWatch(); @@ -982,7 +1018,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`); + debugLog(`[GPS] 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, @@ -995,17 +1031,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`); + debugLog(`[GPS] 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`); + debugLog(`[GPS] GPS accuracy ${state.lastFix.accM}m exceeds threshold (${GPS_ACCURACY_THRESHOLD_M}m), skipping map refresh`); } } catch (e) { - debugError(`primeGpsOnce failed: ${e.message}`); + debugError(`[GPS] primeGpsOnce failed: ${e.message}`); state.gpsState = "error"; // Display GPS error in Dynamic Status Bar setDynamicStatus("GPS error - check permissions", STATUS_COLORS.error); @@ -1062,7 +1098,7 @@ async function deriveChannelKey(channelName) { const hashArray = new Uint8Array(hashBuffer); const channelKey = hashArray.slice(0, 16); - debugLog(`Channel key derived successfully (${channelKey.length} bytes)`); + debugLog(`[CHANNEL] Channel key derived successfully (${channelKey.length} bytes)`); return channelKey; } @@ -1071,25 +1107,25 @@ async function deriveChannelKey(channelName) { async function createWardriveChannel() { if (!state.connection) throw new Error("Not connected"); - debugLog(`Attempting to create channel: ${CHANNEL_NAME}`); + debugLog(`[CHANNEL] Attempting to create channel: ${CHANNEL_NAME}`); // Get all channels const channels = await state.connection.getChannels(); - debugLog(`Retrieved ${channels.length} channels`); + debugLog(`[CHANNEL] Retrieved ${channels.length} channels`); // Find first empty channel slot let emptyIdx = -1; for (let i = 0; i < channels.length; i++) { if (channels[i].name === '') { emptyIdx = i; - debugLog(`Found empty channel slot at index: ${emptyIdx}`); + debugLog(`[CHANNEL] Found empty channel slot at index: ${emptyIdx}`); break; } } // Throw error if no free slots if (emptyIdx === -1) { - debugError(`No empty channel slots available`); + debugError(`[CHANNEL] No empty channel slots available`); throw new Error( `No empty channel slots available. Please free a channel slot on your companion first.` ); @@ -1099,9 +1135,9 @@ async function createWardriveChannel() { const channelKey = await deriveChannelKey(CHANNEL_NAME); // Create the channel - debugLog(`Creating channel ${CHANNEL_NAME} at index ${emptyIdx}`); + debugLog(`[CHANNEL] Creating channel ${CHANNEL_NAME} at index ${emptyIdx}`); await state.connection.setChannel(emptyIdx, CHANNEL_NAME, channelKey); - debugLog(`Channel ${CHANNEL_NAME} created successfully at index ${emptyIdx}`); + debugLog(`[CHANNEL] Channel ${CHANNEL_NAME} created successfully at index ${emptyIdx}`); // Return channel object return { @@ -1113,23 +1149,23 @@ async function createWardriveChannel() { async function ensureChannel() { if (!state.connection) throw new Error("Not connected"); if (state.channel) { - debugLog(`Using existing channel: ${CHANNEL_NAME}`); + debugLog(`[CHANNEL] Using existing channel: ${CHANNEL_NAME}`); return state.channel; } setDynamicStatus("Looking for #wardriving channel", STATUS_COLORS.info); - debugLog(`Looking up channel: ${CHANNEL_NAME}`); + debugLog(`[CHANNEL] Looking up channel: ${CHANNEL_NAME}`); let ch = await state.connection.findChannelByName(CHANNEL_NAME); if (!ch) { setDynamicStatus("Channel #wardriving not found", STATUS_COLORS.info); - debugLog(`Channel ${CHANNEL_NAME} not found, attempting to create it`); + debugLog(`[CHANNEL] Channel ${CHANNEL_NAME} not found, attempting to create it`); try { ch = await createWardriveChannel(); setDynamicStatus("Created #wardriving", STATUS_COLORS.success); - debugLog(`Channel ${CHANNEL_NAME} created successfully`); + debugLog(`[CHANNEL] Channel ${CHANNEL_NAME} created successfully`); } catch (e) { - debugError(`Failed to create channel ${CHANNEL_NAME}: ${e.message}`); + debugError(`[CHANNEL] Failed to create channel ${CHANNEL_NAME}: ${e.message}`); enableControls(false); throw new Error( `Channel ${CHANNEL_NAME} not found and could not be created: ${e.message}` @@ -1137,7 +1173,7 @@ async function ensureChannel() { } } else { setDynamicStatus("Channel #wardriving found", STATUS_COLORS.success); - debugLog(`Channel found: ${CHANNEL_NAME} (index: ${ch.channelIdx})`); + debugLog(`[CHANNEL] Channel found: ${CHANNEL_NAME} (index: ${ch.channelIdx})`); } state.channel = ch; @@ -1194,7 +1230,7 @@ function getDeviceIdentifier() { async function checkCapacity(reason) { // Validate public key exists if (!state.devicePublicKey) { - debugError("checkCapacity called but no public key stored"); + debugError("[CAPACITY] checkCapacity called but no public key stored"); return reason === "connect" ? false : true; // Fail closed on connect, allow disconnect } @@ -1211,7 +1247,7 @@ async function checkCapacity(reason) { reason: reason }; - debugLog(`Checking capacity: reason=${reason}, public_key=${state.devicePublicKey.substring(0, 16)}..., who=${payload.who}`); + debugLog(`[CAPACITY] Checking capacity: reason=${reason}, public_key=${state.devicePublicKey.substring(0, 16)}..., who=${payload.who}`); const response = await fetch(MESHMAPPER_CAPACITY_CHECK_URL, { method: "POST", @@ -1220,10 +1256,10 @@ async function checkCapacity(reason) { }); if (!response.ok) { - debugWarn(`Capacity check API returned error status ${response.status}`); + debugWarn(`[CAPACITY] Capacity check API returned error status ${response.status}`); // Fail closed on network errors for connect if (reason === "connect") { - debugError("Failing closed (denying connection) due to API error"); + debugError("[CAPACITY] Failing closed (denying connection) due to API error"); state.disconnectReason = "app_down"; // Track disconnect reason return false; } @@ -1231,7 +1267,7 @@ async function checkCapacity(reason) { } const data = await response.json(); - debugLog(`Capacity check response: allowed=${data.allowed}, session_id=${data.session_id || 'missing'}`); + debugLog(`[CAPACITY] Capacity check response: allowed=${data.allowed}, session_id=${data.session_id || 'missing'}`); // Handle capacity full vs. allowed cases separately if (data.allowed === false && reason === "connect") { @@ -1242,20 +1278,20 @@ async function checkCapacity(reason) { // For connect requests, validate session_id is present when allowed === true if (reason === "connect" && data.allowed === true) { if (!data.session_id) { - debugError("Capacity check returned allowed=true but session_id is missing"); + debugError("[CAPACITY] Capacity check returned allowed=true but session_id is missing"); state.disconnectReason = "session_id_error"; // Track disconnect reason return false; } // Store the session_id for use in MeshMapper API posts state.wardriveSessionId = data.session_id; - debugLog(`Wardrive session ID received and stored: ${state.wardriveSessionId}`); + debugLog(`[CAPACITY] Wardrive session ID received and stored: ${state.wardriveSessionId}`); } // For disconnect requests, clear the session_id if (reason === "disconnect") { if (state.wardriveSessionId) { - debugLog(`Clearing wardrive session ID on disconnect: ${state.wardriveSessionId}`); + debugLog(`[CAPACITY] Clearing wardrive session ID on disconnect: ${state.wardriveSessionId}`); state.wardriveSessionId = null; } } @@ -1263,11 +1299,11 @@ async function checkCapacity(reason) { return data.allowed === true; } catch (error) { - debugError(`Capacity check failed: ${error.message}`); + debugError(`[CAPACITY] Capacity check failed: ${error.message}`); // Fail closed on network errors for connect if (reason === "connect") { - debugError("Failing closed (denying connection) due to network error"); + debugError("[CAPACITY] Failing closed (denying connection) due to network error"); state.disconnectReason = "app_down"; // Track disconnect reason return false; } @@ -1286,12 +1322,12 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { try { // Validate session_id exists before posting if (!state.wardriveSessionId) { - debugError("Cannot post to MeshMapper API: no session_id available"); + debugError("[API QUEUE] Cannot post to MeshMapper API: no session_id available"); setDynamicStatus("Error: No session ID for API post", STATUS_COLORS.error); state.disconnectReason = "session_id_error"; // Track disconnect reason // Disconnect after a brief delay to ensure user sees the error message setTimeout(() => { - disconnect().catch(err => debugError(`Disconnect after missing session_id failed: ${err.message}`)); + disconnect().catch(err => debugError(`[BLE] Disconnect after missing session_id failed: ${err.message}`)); }, 1500); return; // Exit early } @@ -1310,7 +1346,7 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { WARDRIVE_TYPE: "TX" }; - debugLog(`Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}, heard_repeats=${heardRepeats}, ver=${payload.ver}, iata=${payload.iata}, session_id=${payload.session_id}, WARDRIVE_TYPE=${payload.WARDRIVE_TYPE}`); + debugLog(`[API QUEUE] Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}, heard_repeats=${heardRepeats}, ver=${payload.ver}, iata=${payload.iata}, session_id=${payload.session_id}, WARDRIVE_TYPE=${payload.WARDRIVE_TYPE}`); const response = await fetch(MESHMAPPER_API_URL, { method: "POST", @@ -1318,42 +1354,42 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { body: JSON.stringify(payload) }); - debugLog(`MeshMapper API response status: ${response.status}`); + debugLog(`[API QUEUE] MeshMapper API response status: ${response.status}`); // Always try to parse the response body to check for slot revocation // regardless of HTTP status code try { const data = await response.json(); - debugLog(`MeshMapper API response data: ${JSON.stringify(data)}`); + debugLog(`[API QUEUE] MeshMapper API response data: ${JSON.stringify(data)}`); // Check if slot has been revoked if (data.allowed === false) { - debugWarn("MeshMapper API returned allowed=false, WarDriving slot has been revoked, disconnecting"); + debugWarn("[API QUEUE] MeshMapper API returned allowed=false, WarDriving slot has been revoked, disconnecting"); setDynamicStatus("Error: Posting to API (Revoked)", STATUS_COLORS.error); state.disconnectReason = "slot_revoked"; // Track disconnect reason // Disconnect after a brief delay to ensure user sees the error message setTimeout(() => { - disconnect().catch(err => debugError(`Disconnect after slot revocation failed: ${err.message}`)); + disconnect().catch(err => debugError(`[BLE] Disconnect after slot revocation failed: ${err.message}`)); }, 1500); return; // Exit early after slot revocation } else if (data.allowed === true) { - debugLog("MeshMapper API allowed check passed: device still has an active WarDriving slot"); + debugLog("[API QUEUE] MeshMapper API allowed check passed: device still has an active WarDriving slot"); } else { - debugWarn(`MeshMapper API response missing 'allowed' field: ${JSON.stringify(data)}`); + debugWarn(`[API QUEUE] MeshMapper API response missing 'allowed' field: ${JSON.stringify(data)}`); } } catch (parseError) { - debugWarn(`Failed to parse MeshMapper API response: ${parseError.message}`); + debugWarn(`[API QUEUE] Failed to parse MeshMapper API response: ${parseError.message}`); // Continue operation if we can't parse the response } if (!response.ok) { - debugWarn(`MeshMapper API returned error status ${response.status}`); + debugWarn(`[API QUEUE] MeshMapper API returned error status ${response.status}`); } else { - debugLog(`MeshMapper API post successful (status ${response.status})`); + debugLog(`[API QUEUE] MeshMapper API post successful (status ${response.status})`); } } catch (error) { // Log error but don't fail the ping - debugError(`MeshMapper API post failed: ${error.message}`); + debugError(`[API QUEUE] MeshMapper API post failed: ${error.message}`); } } @@ -1367,33 +1403,33 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { * @param {string} heardRepeats - Heard repeats string (e.g., "4e(1.75),b7(-0.75)" or "None") */ async function postApiInBackground(lat, lon, accuracy, heardRepeats) { - debugLog(`postApiInBackground called with heard_repeats="${heardRepeats}"`); + debugLog(`[API QUEUE] postApiInBackground called with heard_repeats="${heardRepeats}"`); // Hidden 3-second delay before API POST (no user-facing status message) - debugLog("Starting 3-second delay before API POST"); + debugLog("[API QUEUE] Starting 3-second delay before API POST"); await new Promise(resolve => setTimeout(resolve, 3000)); - debugLog("3-second delay complete, posting to API"); + debugLog("[API QUEUE] 3-second delay complete, posting to API"); try { await postToMeshMapperAPI(lat, lon, heardRepeats); - debugLog("Background API post completed successfully"); + debugLog("[API QUEUE] Background API post completed successfully"); // No success status message - suppress from UI } catch (error) { - debugError("Background API post failed:", error); + debugError("[API QUEUE] Background API post failed:", error); // Errors are propagated to caller for user notification throw error; } // Update map after API post - debugLog("Scheduling coverage map refresh"); + debugLog("[UI] Scheduling coverage map refresh"); setTimeout(() => { const shouldRefreshMap = accuracy && accuracy < GPS_ACCURACY_THRESHOLD_M; if (shouldRefreshMap) { - debugLog(`Refreshing coverage map (accuracy ${accuracy}m within threshold)`); + debugLog(`[UI] Refreshing coverage map (accuracy ${accuracy}m within threshold)`); scheduleCoverageRefresh(lat, lon); } else { - debugLog(`Skipping map refresh (accuracy ${accuracy}m exceeds threshold)`); + debugLog(`[UI] Skipping map refresh (accuracy ${accuracy}m exceeds threshold)`); } }, MAP_REFRESH_DELAY_MS); } @@ -1407,7 +1443,7 @@ async function postApiInBackground(lat, lon, accuracy, heardRepeats) { * @param {string} heardRepeats - Heard repeats string (e.g., "4e(1.75),b7(-0.75)" or "None") */ async function postApiAndRefreshMap(lat, lon, accuracy, heardRepeats) { - debugLog(`postApiAndRefreshMap called with heard_repeats="${heardRepeats}"`); + debugLog(`[API QUEUE] postApiAndRefreshMap called with heard_repeats="${heardRepeats}"`); // Build payload const payload = { @@ -1425,17 +1461,17 @@ async function postApiAndRefreshMap(lat, lon, accuracy, heardRepeats) { // Queue message instead of posting immediately queueApiMessage(payload, "TX"); - debugLog(`TX message queued: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, heard_repeats="${heardRepeats}"`); + debugLog(`[API QUEUE] TX message queued: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, heard_repeats="${heardRepeats}"`); // Update map after queueing setTimeout(() => { const shouldRefreshMap = accuracy && accuracy < GPS_ACCURACY_THRESHOLD_M; if (shouldRefreshMap) { - debugLog(`Refreshing coverage map (accuracy ${accuracy}m within threshold)`); + debugLog(`[UI] Refreshing coverage map (accuracy ${accuracy}m within threshold)`); scheduleCoverageRefresh(lat, lon); } else { - debugLog(`Skipping map refresh (accuracy ${accuracy}m exceeds threshold)`); + debugLog(`[UI] Skipping map refresh (accuracy ${accuracy}m exceeds threshold)`); } // Unlock ping controls now that message is queued @@ -1448,13 +1484,13 @@ async function postApiAndRefreshMap(lat, lon, accuracy, heardRepeats) { const resumed = resumeAutoCountdown(); if (!resumed) { // No paused timer to resume, schedule new auto ping (this was an auto ping) - debugLog("Scheduling next auto ping"); + debugLog("[AUTO] Scheduling next auto ping"); scheduleNextAutoPing(); } else { - debugLog("Resumed auto countdown after manual ping"); + debugLog("[AUTO] Resumed auto countdown after manual ping"); } } else { - debugLog("Setting dynamic status to show queue size"); + debugLog("[AUTO] Setting dynamic status to show queue size"); // Status already set by queueApiMessage() } } @@ -1496,8 +1532,7 @@ function queueApiMessage(payload, wardriveType) { flushApiQueue(); } - // Update status to show queue depth - setDynamicStatus(`Queued (${apiQueue.messages.length}/50)`, STATUS_COLORS.info); + // Queue depth is logged above for debugging - no need to show in dynamic status bar } /** @@ -1596,8 +1631,7 @@ async function flushApiQueue() { const rxCount = batch.filter(m => m.WARDRIVE_TYPE === "RX").length; debugLog(`[API QUEUE] Batch composition: ${txCount} TX, ${rxCount} RX`); - // Update status - setDynamicStatus(`Posting ${batch.length} to API`, STATUS_COLORS.info); + // Status removed from dynamic status bar - debug log above is sufficient for debugging try { // Validate session_id exists @@ -1606,7 +1640,7 @@ async function flushApiQueue() { setDynamicStatus("Error: No session ID for API post", STATUS_COLORS.error); state.disconnectReason = "session_id_error"; setTimeout(() => { - disconnect().catch(err => debugError(`Disconnect after missing session_id failed: ${err.message}`)); + disconnect().catch(err => debugError(`[BLE] Disconnect after missing session_id failed: ${err.message}`)); }, 1500); return; } @@ -1632,7 +1666,7 @@ async function flushApiQueue() { setDynamicStatus("Error: Posting to API (Revoked)", STATUS_COLORS.error); state.disconnectReason = "slot_revoked"; setTimeout(() => { - disconnect().catch(err => debugError(`Disconnect after slot revocation failed: ${err.message}`)); + disconnect().catch(err => debugError(`[BLE] Disconnect after slot revocation failed: ${err.message}`)); }, 1500); return; } else if (data.allowed === true) { @@ -1810,19 +1844,19 @@ async function decryptGroupTextPayload(payload, channelKey) { * @param {number} channelIdx - The channel index where the ping was sent */ function startRepeaterTracking(payload, channelIdx) { - debugLog(`Starting repeater echo tracking for ping: "${payload}" on channel ${channelIdx}`); - debugLog(`7-second rx_log listening window opened at ${new Date().toISOString()}`); + debugLog(`[PING] Starting repeater echo tracking for ping: "${payload}" on channel ${channelIdx}`); + debugLog(`[PING] 7-second rx_log listening window opened at ${new Date().toISOString()}`); // Verify we have the channel hash if (WARDRIVING_CHANNEL_HASH === null) { - debugError(`Cannot start repeater tracking: channel hash not initialized`); + debugError(`[PING] Cannot start repeater tracking: channel hash not initialized`); return; } // Clear any existing tracking state stopRepeaterTracking(); - debugLog(`Using pre-computed channel hash for correlation: 0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')}`); + debugLog(`[PING] Using pre-computed channel hash for correlation: 0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')}`); // Initialize tracking state state.repeaterTracking.isListening = true; @@ -1831,7 +1865,7 @@ function startRepeaterTracking(payload, channelIdx) { state.repeaterTracking.channelIdx = channelIdx; state.repeaterTracking.repeaters.clear(); - debugLog(`Session Log tracking activated - unified handler will delegate echoes to Session Log`); + debugLog(`[SESSION LOG] Session Log tracking activated - unified handler will delegate echoes to Session Log`); // Note: The unified RX handler (started at connect) will automatically delegate to // handleSessionLogTracking() when isListening = true. No separate handler needed. @@ -1931,16 +1965,16 @@ async function handleSessionLogTracking(packet, data) { const firstHopId = packet.path[0]; const pathHex = firstHopId.toString(16).padStart(2, '0'); - debugLog(`Repeater echo accepted: first_hop=${pathHex}, SNR=${data.lastSnr}, full_path_length=${packet.path.length}`); + debugLog(`[PING] Repeater echo accepted: first_hop=${pathHex}, SNR=${data.lastSnr}, full_path_length=${packet.path.length}`); // Check if we already have this path if (state.repeaterTracking.repeaters.has(pathHex)) { const existing = state.repeaterTracking.repeaters.get(pathHex); - debugLog(`Deduplication: path ${pathHex} already seen (existing SNR=${existing.snr}, new SNR=${data.lastSnr})`); + debugLog(`[PING] Deduplication: path ${pathHex} already seen (existing SNR=${existing.snr}, new SNR=${data.lastSnr})`); // Keep the best (highest) SNR if (data.lastSnr > existing.snr) { - debugLog(`Deduplication decision: updating path ${pathHex} with better SNR: ${existing.snr} -> ${data.lastSnr}`); + debugLog(`[PING] Deduplication decision: updating path ${pathHex} with better SNR: ${existing.snr} -> ${data.lastSnr}`); state.repeaterTracking.repeaters.set(pathHex, { snr: data.lastSnr, seenCount: existing.seenCount + 1 @@ -1949,13 +1983,13 @@ async function handleSessionLogTracking(packet, data) { // Trigger incremental UI update since SNR changed updateCurrentLogEntryWithLiveRepeaters(); } else { - debugLog(`Deduplication decision: keeping existing SNR for path ${pathHex} (existing ${existing.snr} >= new ${data.lastSnr})`); + debugLog(`[PING] Deduplication decision: keeping existing SNR for path ${pathHex} (existing ${existing.snr} >= new ${data.lastSnr})`); // Still increment seen count existing.seenCount++; } } else { // New path - debugLog(`Adding new repeater echo: path=${pathHex}, SNR=${data.lastSnr}`); + debugLog(`[PING] Adding new repeater echo: path=${pathHex}, SNR=${data.lastSnr}`); state.repeaterTracking.repeaters.set(pathHex, { snr: data.lastSnr, seenCount: 1 @@ -1984,7 +2018,7 @@ function stopRepeaterTracking() { return []; } - debugLog(`Stopping repeater echo tracking`); + debugLog(`[PING] Stopping repeater echo tracking`); // No need to unregister handler - unified handler continues running // Just clear the tracking state @@ -1998,7 +2032,7 @@ function stopRepeaterTracking() { // Sort by repeater ID for deterministic output repeaters.sort((a, b) => a.repeaterId.localeCompare(b.repeaterId)); - debugLog(`Final aggregated repeater list: ${repeaters.length > 0 ? repeaters.map(r => `${r.repeaterId}(${r.snr}dB)`).join(', ') : 'none'}`); + debugLog(`[PING] Final aggregated repeater list: ${repeaters.length > 0 ? repeaters.map(r => `${r.repeaterId}(${r.snr}dB)`).join(', ') : 'none'}`); // Reset state state.repeaterTracking.isListening = false; @@ -2529,7 +2563,7 @@ function createChipElement(type, value) { * @returns {HTMLElement} Log entry element */ function createLogEntryElement(entry) { - debugLog(`Creating log entry element for timestamp: ${entry.timestamp}`); + debugLog(`[UI] Creating log entry element for timestamp: ${entry.timestamp}`); const logEntry = document.createElement('div'); logEntry.className = 'logEntry'; @@ -2559,20 +2593,20 @@ function createLogEntryElement(entry) { noneSpan.className = 'text-xs text-slate-500 italic'; noneSpan.textContent = 'No repeats heard'; chipsRow.appendChild(noneSpan); - debugLog(`Log entry has no events (no repeats heard)`); + debugLog(`[UI] Log entry has no events (no repeats heard)`); } else { - debugLog(`Log entry has ${entry.events.length} event(s)`); + debugLog(`[UI] Log entry has ${entry.events.length} event(s)`); entry.events.forEach(event => { const chip = createChipElement(event.type, event.value); chipsRow.appendChild(chip); - debugLog(`Added chip for repeater ${event.type} with SNR ${event.value} dB`); + debugLog(`[UI] Added chip for repeater ${event.type} with SNR ${event.value} dB`); }); } logEntry.appendChild(topRow); logEntry.appendChild(chipsRow); - debugLog(`Log entry element created successfully with class: ${logEntry.className}`); + debugLog(`[UI] Log entry element created successfully with class: ${logEntry.className}`); return logEntry; } @@ -2588,7 +2622,7 @@ function updateLogSummary() { if (count === 0) { logLastTime.textContent = 'No data'; logLastSnr.textContent = '—'; - debugLog('Session log summary updated: no entries'); + debugLog('[UI] Session log summary updated: no entries'); return; } @@ -2598,7 +2632,7 @@ function updateLogSummary() { // Count total heard repeats in the latest ping const heardCount = lastEntry.events.length; - debugLog(`Session log summary updated: ${count} total pings, latest ping heard ${heardCount} repeats`); + debugLog(`[UI] Session log summary updated: ${count} total pings, latest ping heard ${heardCount} repeats`); if (heardCount > 0) { logLastSnr.textContent = heardCount === 1 ? '1 Repeat' : `${heardCount} Repeats`; @@ -2615,7 +2649,7 @@ function updateLogSummary() { function renderLogEntries() { if (!sessionPingsEl) return; - debugLog(`Rendering ${sessionLogState.entries.length} log entries`); + debugLog(`[UI] Rendering ${sessionLogState.entries.length} log entries`); sessionPingsEl.innerHTML = ''; if (sessionLogState.entries.length === 0) { @@ -2624,7 +2658,7 @@ function renderLogEntries() { placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; placeholder.textContent = 'No pings logged yet'; sessionPingsEl.appendChild(placeholder); - debugLog(`Rendered placeholder (no entries)`); + debugLog(`[UI] Rendered placeholder (no entries)`); return; } @@ -2634,16 +2668,16 @@ function renderLogEntries() { entries.forEach((entry, index) => { const element = createLogEntryElement(entry); sessionPingsEl.appendChild(element); - debugLog(`Appended log entry ${index + 1}/${entries.length} to sessionPingsEl`); + debugLog(`[UI] Appended log entry ${index + 1}/${entries.length} to sessionPingsEl`); }); // Auto-scroll to top (newest) if (sessionLogState.autoScroll && logScrollContainer) { logScrollContainer.scrollTop = 0; - debugLog(`Auto-scrolled to top of log container`); + debugLog(`[UI] Auto-scrolled to top of log container`); } - debugLog(`Finished rendering all log entries`); + debugLog(`[UI] Finished rendering all log entries`); } /** @@ -2776,37 +2810,61 @@ function updateRxLogSummary() { /** * Render all RX log entries */ -function renderRxLogEntries() { +/** + * Render RX log entries (full render or incremental) + * @param {boolean} fullRender - If true, re-render all entries. If false, only render new entries. + */ +function renderRxLogEntries(fullRender = false) { if (!rxLogEntries) return; - debugLog(`[PASSIVE RX UI] Rendering ${rxLogState.entries.length} RX log entries`); - rxLogEntries.innerHTML = ''; - - if (rxLogState.entries.length === 0) { - const placeholder = document.createElement('div'); - placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; - placeholder.textContent = 'No RX observations yet'; - rxLogEntries.appendChild(placeholder); - debugLog(`[PASSIVE RX UI] Rendered placeholder (no entries)`); - return; + if (fullRender) { + debugLog(`[PASSIVE RX UI] Full render of ${rxLogState.entries.length} RX log entries`); + rxLogEntries.innerHTML = ''; + + if (rxLogState.entries.length === 0) { + const placeholder = document.createElement('div'); + placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; + placeholder.textContent = 'No RX observations yet'; + rxLogEntries.appendChild(placeholder); + debugLog(`[PASSIVE RX UI] Rendered placeholder (no entries)`); + return; + } + + // Render newest first + const entries = [...rxLogState.entries].reverse(); + + entries.forEach((entry, index) => { + const element = createRxLogEntryElement(entry); + rxLogEntries.appendChild(element); + }); + + debugLog(`[PASSIVE RX UI] Full render complete: ${entries.length} entries`); + } else { + // Incremental render: only add the newest entry + if (rxLogState.entries.length === 0) { + debugLog(`[PASSIVE RX UI] No entries to render incrementally`); + return; + } + + // Remove placeholder if it exists + const placeholder = rxLogEntries.querySelector('.text-xs.text-slate-500.italic'); + if (placeholder) { + placeholder.remove(); + } + + // Get the newest entry (last in array) and prepend it (newest first display) + const newestEntry = rxLogState.entries[rxLogState.entries.length - 1]; + const element = createRxLogEntryElement(newestEntry); + rxLogEntries.insertBefore(element, rxLogEntries.firstChild); + + debugLog(`[PASSIVE RX UI] Appended entry ${rxLogState.entries.length}/${rxLogState.entries.length}`); } - // Render newest first - const entries = [...rxLogState.entries].reverse(); - - entries.forEach((entry, index) => { - const element = createRxLogEntryElement(entry); - rxLogEntries.appendChild(element); - debugLog(`[PASSIVE RX UI] Appended entry ${index + 1}/${entries.length}`); - }); - // Auto-scroll to top (newest) if (rxLogState.autoScroll && rxLogScrollContainer) { rxLogScrollContainer.scrollTop = 0; debugLog(`[PASSIVE RX UI] Auto-scrolled to top`); } - - debugLog(`[PASSIVE RX UI] Finished rendering all entries`); } /** @@ -2858,14 +2916,206 @@ function addRxLogEntry(repeaterId, snr, lat, lon, timestamp) { if (rxLogState.entries.length > rxLogState.maxEntries) { const removed = rxLogState.entries.shift(); debugLog(`[PASSIVE RX UI] Max entries limit reached, removed oldest entry (repeater=${removed.repeaterId})`); + // Need full re-render when removing old entries + renderRxLogEntries(true); + } else { + // Incremental render - only append the new entry + renderRxLogEntries(false); } - renderRxLogEntries(); updateRxLogSummary(); debugLog(`[PASSIVE RX UI] Added entry: repeater=${repeaterId}, snr=${snr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); } +// ---- Error Log ---- + +/** + * Create DOM element for Error log entry + * @param {Object} entry - Error log entry object + * @returns {HTMLElement} DOM element for the error log entry + */ +function createErrorLogEntryElement(entry) { + const logEntry = document.createElement('div'); + logEntry.className = 'logEntry'; + + // Top row: time + error type/source + const topRow = document.createElement('div'); + topRow.className = 'logRowTop'; + + const time = document.createElement('span'); + time.className = 'logTime'; + const date = new Date(entry.timestamp); + time.textContent = date.toLocaleTimeString(); + + const source = document.createElement('span'); + source.className = 'text-xs font-mono text-red-400'; + source.textContent = entry.source || 'ERROR'; + + topRow.appendChild(time); + topRow.appendChild(source); + + // Message row + const messageRow = document.createElement('div'); + messageRow.className = 'text-xs text-red-300 break-words mt-1'; + messageRow.textContent = entry.message; + + logEntry.appendChild(topRow); + logEntry.appendChild(messageRow); + + return logEntry; +} + +/** + * Update error log summary bar + */ +function updateErrorLogSummary() { + if (!errorLogCount || !errorLogLastTime) return; + + const count = errorLogState.entries.length; + + if (count === 0) { + errorLogCount.textContent = '0 errors'; + errorLogLastTime.textContent = 'No errors'; + errorLogLastTime.classList.add('hidden'); + if (errorLogLastError) { + errorLogLastError.textContent = '—'; + } + debugLog('[ERROR LOG] Summary updated: no entries'); + return; + } + + const lastEntry = errorLogState.entries[errorLogState.entries.length - 1]; + errorLogCount.textContent = `${count} error${count !== 1 ? 's' : ''}`; + + const date = new Date(lastEntry.timestamp); + errorLogLastTime.textContent = date.toLocaleTimeString(); + errorLogLastTime.classList.remove('hidden'); + + if (errorLogLastError) { + // Show first 20 chars of error message + const preview = lastEntry.message.length > 20 + ? lastEntry.message.substring(0, 20) + '...' + : lastEntry.message; + errorLogLastError.textContent = preview; + } + + debugLog(`[ERROR LOG] Summary updated: ${count} errors, last: ${lastEntry.message.substring(0, 30)}...`); +} + +/** + * Render Error log entries (full render or incremental) + * @param {boolean} fullRender - If true, re-render all entries. If false, only render new entries. + */ +function renderErrorLogEntries(fullRender = false) { + if (!errorLogEntries) return; + + if (fullRender) { + debugLog(`[ERROR LOG] Full render of ${errorLogState.entries.length} error log entries`); + errorLogEntries.innerHTML = ''; + + if (errorLogState.entries.length === 0) { + const placeholder = document.createElement('div'); + placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; + placeholder.textContent = 'No errors logged'; + errorLogEntries.appendChild(placeholder); + debugLog(`[ERROR LOG] Rendered placeholder (no entries)`); + return; + } + + // Render newest first + const entries = [...errorLogState.entries].reverse(); + + entries.forEach((entry, index) => { + const element = createErrorLogEntryElement(entry); + errorLogEntries.appendChild(element); + }); + + debugLog(`[ERROR LOG] Full render complete: ${entries.length} entries`); + } else { + // Incremental render: only add the newest entry + if (errorLogState.entries.length === 0) { + debugLog(`[ERROR LOG] No entries to render incrementally`); + return; + } + + // Remove placeholder if it exists + const placeholder = errorLogEntries.querySelector('.text-xs.text-slate-500.italic'); + if (placeholder) { + placeholder.remove(); + } + + // Get the newest entry (last in array) and prepend it (newest first display) + const newestEntry = errorLogState.entries[errorLogState.entries.length - 1]; + const element = createErrorLogEntryElement(newestEntry); + errorLogEntries.insertBefore(element, errorLogEntries.firstChild); + + debugLog(`[ERROR LOG] Appended entry ${errorLogState.entries.length}/${errorLogState.entries.length}`); + } + + // Auto-scroll to top (newest) + if (errorLogState.autoScroll && errorLogScrollContainer) { + errorLogScrollContainer.scrollTop = 0; + debugLog(`[ERROR LOG] Auto-scrolled to top`); + } +} + +/** + * Toggle Error log expanded/collapsed + */ +function toggleErrorLogBottomSheet() { + errorLogState.isExpanded = !errorLogState.isExpanded; + + if (errorLogBottomSheet) { + if (errorLogState.isExpanded) { + errorLogBottomSheet.classList.add('open'); + errorLogBottomSheet.classList.remove('hidden'); + } else { + errorLogBottomSheet.classList.remove('open'); + errorLogBottomSheet.classList.add('hidden'); + } + } + + // Toggle arrow rotation + if (errorLogExpandArrow) { + if (errorLogState.isExpanded) { + errorLogExpandArrow.classList.add('expanded'); + } else { + errorLogExpandArrow.classList.remove('expanded'); + } + } +} + +/** + * Add entry to Error log + * @param {string} message - Error message + * @param {string} source - Optional source/context of the error + */ +function addErrorLogEntry(message, source = null) { + const entry = { + message, + source, + timestamp: new Date().toISOString() + }; + + errorLogState.entries.push(entry); + + // Apply max entries limit + if (errorLogState.entries.length > errorLogState.maxEntries) { + const removed = errorLogState.entries.shift(); + debugLog(`[ERROR LOG] Max entries limit reached, removed oldest entry`); + // Need full re-render when removing old entries + renderErrorLogEntries(true); + } else { + // Incremental render - only append the new entry + renderErrorLogEntries(false); + } + + updateErrorLogSummary(); + + debugLog(`[ERROR LOG] Added entry: ${message.substring(0, 50)}${message.length > 50 ? '...' : ''}`); +} + // ---- Ping ---- /** * Acquire fresh GPS coordinates and update state @@ -2879,7 +3129,7 @@ async function acquireFreshGpsPosition() { 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`); + debugLog(`[GPS] Fresh GPS acquired: lat=${coords.lat.toFixed(5)}, lon=${coords.lon.toFixed(5)}, accuracy=${coords.accuracy}m`); state.lastFix = { lat: coords.lat, @@ -2901,7 +3151,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { if (isAutoMode) { // Auto mode: validate GPS freshness before sending if (!state.lastFix) { - debugWarn("Auto ping skipped: no GPS fix available yet"); + debugWarn("[AUTO] Auto ping skipped: no GPS fix available yet"); setDynamicStatus("Waiting for GPS fix", STATUS_COLORS.warning); return null; } @@ -2912,20 +3162,20 @@ async function getGpsCoordinatesForPing(isAutoMode) { const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; if (ageMs >= maxAge) { - debugLog(`GPS data too old for auto ping (${ageMs}ms), attempting to refresh`); + debugLog(`[GPS] GPS data too old for auto ping (${ageMs}ms), attempting to refresh`); setDynamicStatus("GPS data too old, requesting fresh position", STATUS_COLORS.warning); try { return await acquireFreshGpsPosition(); } catch (e) { - debugError(`Could not refresh GPS position for auto ping: ${e.message}`, e); + debugError(`[GPS] Could not refresh GPS position for auto ping: ${e.message}`, e); // Set skip reason so the countdown will show the appropriate message state.skipReason = "gps too old"; return null; } } - debugLog(`Using GPS watch data: lat=${state.lastFix.lat.toFixed(5)}, lon=${state.lastFix.lon.toFixed(5)} (age: ${ageMs}ms)`); + debugLog(`[GPS] 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, @@ -2942,7 +3192,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { // 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)`); + debugLog(`[GPS] Using GPS watch data for manual ping (age: ${ageMs}ms, watch active)`); return { lat: state.lastFix.lat, lon: state.lastFix.lon, @@ -2955,7 +3205,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { const intervalMs = getSelectedIntervalMs(); const maxAge = intervalMs + GPS_FRESHNESS_BUFFER_MS; if (ageMs < maxAge) { - debugLog(`Using cached GPS data (age: ${ageMs}ms, watch inactive)`); + debugLog(`[GPS] Using cached GPS data (age: ${ageMs}ms, watch inactive)`); return { lat: state.lastFix.lat, lon: state.lastFix.lon, @@ -2965,16 +3215,16 @@ async function getGpsCoordinatesForPing(isAutoMode) { } // Data exists but is too old - debugLog(`GPS data too old (${ageMs}ms), requesting fresh position`); + debugLog(`[GPS] GPS data too old (${ageMs}ms), requesting fresh position`); setDynamicStatus("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"); + debugLog("[GPS] Requesting fresh GPS position for manual ping"); try { return await acquireFreshGpsPosition(); } catch (e) { - debugError(`Could not get fresh GPS location: ${e.message}`, e); + debugError(`[GPS] 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"); } @@ -3038,7 +3288,7 @@ function updatePingLogWithRepeaters(logData, repeaters) { } } - debugLog(`Updated ping log entry with repeater telemetry: ${repeaterStr}`); + debugLog(`[PING] Updated ping log entry with repeater telemetry: ${repeaterStr}`); } /** @@ -3065,7 +3315,7 @@ function updateCurrentLogEntryWithLiveRepeaters() { // Reuse the existing updatePingLogWithRepeaters function updatePingLogWithRepeaters(logData, repeaters); - debugLog(`Incrementally updated ping log entry: ${repeaters.length} repeater(s) detected so far`); + debugLog(`[PING] Incrementally updated ping log entry: ${repeaters.length} repeater(s) detected so far`); } /** @@ -3073,12 +3323,12 @@ function updateCurrentLogEntryWithLiveRepeaters() { * @param {boolean} manual - Whether this is a manual ping (true) or auto ping (false) */ async function sendPing(manual = false) { - debugLog(`sendPing called (manual=${manual})`); + debugLog(`[PING] sendPing called (manual=${manual})`); try { // Check cooldown only for manual pings if (manual && isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); - debugLog(`Manual ping blocked by cooldown (${remainingSec}s remaining)`); + debugLog(`[PING] Manual ping blocked by cooldown (${remainingSec}s remaining)`); setDynamicStatus(`Wait ${remainingSec}s before sending another ping`, STATUS_COLORS.warning); return; } @@ -3086,7 +3336,7 @@ async function sendPing(manual = false) { // 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"); + debugLog("[PING] Manual ping during auto mode - pausing auto countdown"); pauseAutoCountdown(); setDynamicStatus("Sending manual ping", STATUS_COLORS.info); } else if (!manual && state.running) { @@ -3116,9 +3366,9 @@ async function sendPing(manual = false) { const { lat, lon, accuracy } = coords; // VALIDATION 1: Geofence check (FIRST - must be within Ottawa 150km) - debugLog("Starting geofence validation"); + debugLog("[PING] Starting geofence validation"); if (!validateGeofence(lat, lon)) { - debugLog("Ping blocked: outside geofence"); + debugLog("[PING] Ping blocked: outside geofence"); // Set skip reason for auto mode countdown display state.skipReason = "outside geofence"; @@ -3135,12 +3385,12 @@ async function sendPing(manual = false) { return; } - debugLog("Geofence validation passed"); + debugLog("[PING] Geofence validation passed"); // VALIDATION 2: Distance check (SECOND - must be ≥ 25m from last successful ping) - debugLog("Starting distance validation"); + debugLog("[PING] Starting distance validation"); if (!validateMinimumDistance(lat, lon)) { - debugLog("Ping blocked: too close to last ping"); + debugLog("[PING] Ping blocked: too close to last ping"); // Set skip reason for auto mode countdown display state.skipReason = "too close"; @@ -3157,41 +3407,41 @@ async function sendPing(manual = false) { return; } - debugLog("Distance validation passed"); + debugLog("[PING] Distance validation passed"); // Both validations passed - execute ping operation (Mesh + API) - debugLog("All validations passed, executing ping operation"); + debugLog("[PING] All validations passed, executing ping operation"); // Lock ping controls for the entire ping lifecycle (until API post completes) state.pingInProgress = true; updateControlsForCooldown(); - debugLog("Ping controls locked (pingInProgress=true)"); + debugLog("[PING] Ping controls locked (pingInProgress=true)"); const payload = buildPayload(lat, lon); - debugLog(`Sending ping to channel: "${payload}"`); + debugLog(`[PING] Sending ping to channel: "${payload}"`); const ch = await ensureChannel(); // Capture GPS coordinates at ping time - these will be used for API post after 10s delay state.capturedPingCoords = { lat, lon, accuracy }; - debugLog(`GPS coordinates captured at ping time: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, accuracy=${accuracy}m`); + debugLog(`[PING] GPS coordinates captured at ping time: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, accuracy=${accuracy}m`); // Start repeater echo tracking BEFORE sending the ping - debugLog(`Channel ping transmission: timestamp=${new Date().toISOString()}, channel=${ch.channelIdx}, payload="${payload}"`); + debugLog(`[PING] Channel ping transmission: timestamp=${new Date().toISOString()}, channel=${ch.channelIdx}, payload="${payload}"`); startRepeaterTracking(payload, ch.channelIdx); await state.connection.sendChannelTextMessage(ch.channelIdx, payload); - debugLog(`Ping sent successfully to channel ${ch.channelIdx}`); + debugLog(`[PING] 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)})`); + debugLog(`[PING] 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`); + debugLog(`[PING] Starting ${COOLDOWN_MS}ms cooldown`); startCooldown(); // Update status after ping is sent @@ -3206,7 +3456,7 @@ async function sendPing(manual = false) { // Start RX listening countdown // The minimum 500ms visibility of "Ping sent" is enforced by setStatus() if (state.connection) { - debugLog(`Starting RX listening window for ${RX_LOG_LISTEN_WINDOW_MS}ms`); + debugLog(`[PING] Starting RX listening window for ${RX_LOG_LISTEN_WINDOW_MS}ms`); startRxListeningCountdown(RX_LOG_LISTEN_WINDOW_MS); } @@ -3215,21 +3465,21 @@ async function sendPing(manual = false) { // Capture coordinates locally to prevent race conditions with concurrent pings const capturedCoords = state.capturedPingCoords; state.meshMapperTimer = setTimeout(async () => { - debugLog(`RX listening window completed after ${RX_LOG_LISTEN_WINDOW_MS}ms`); + debugLog(`[PING] RX listening window completed after ${RX_LOG_LISTEN_WINDOW_MS}ms`); // Stop listening countdown stopRxListeningCountdown(); // Stop repeater tracking and get final results const repeaters = stopRepeaterTracking(); - debugLog(`Finalized heard repeats: ${repeaters.length} unique paths detected`); + debugLog(`[PING] Finalized heard repeats: ${repeaters.length} unique paths detected`); // Update UI log with repeater data updatePingLogWithRepeaters(logEntry, repeaters); // Format repeater data for API const heardRepeatsStr = formatRepeaterTelemetry(repeaters); - debugLog(`Formatted heard_repeats for API: "${heardRepeatsStr}"`); + debugLog(`[PING] Formatted heard_repeats for API: "${heardRepeatsStr}"`); // Update status and start next timer IMMEDIATELY (before API post) // This is the key change: we don't wait for API to complete @@ -3239,13 +3489,13 @@ async function sendPing(manual = false) { const resumed = resumeAutoCountdown(); if (!resumed) { // No paused timer to resume, schedule new auto ping (this was an auto ping) - debugLog("Scheduling next auto ping immediately after RX window"); + debugLog("[AUTO] Scheduling next auto ping immediately after RX window"); scheduleNextAutoPing(); } else { - debugLog("Resumed auto countdown after manual ping"); + debugLog("[AUTO] Resumed auto countdown after manual ping"); } } else { - debugLog("Setting dynamic status to Idle (manual mode)"); + debugLog("[UI] Setting dynamic status to Idle (manual mode)"); setDynamicStatus("Idle"); } } @@ -3257,18 +3507,18 @@ async function sendPing(manual = false) { // Use captured coordinates for API post (not current GPS position) if (capturedCoords) { const { lat: apiLat, lon: apiLon, accuracy: apiAccuracy } = capturedCoords; - debugLog(`Backgrounding API post for coordinates: lat=${apiLat.toFixed(5)}, lon=${apiLon.toFixed(5)}, accuracy=${apiAccuracy}m`); + debugLog(`[API QUEUE] Backgrounding API post for coordinates: lat=${apiLat.toFixed(5)}, lon=${apiLon.toFixed(5)}, accuracy=${apiAccuracy}m`); // Post to API in background (async, fire-and-forget with error handling) postApiInBackground(apiLat, apiLon, apiAccuracy, heardRepeatsStr).catch(error => { - debugError(`Background API post failed: ${error.message}`, error); + debugError(`[API QUEUE] Background API post failed: ${error.message}`, error); // Show error to user only if API fails setDynamicStatus("Error: API post failed", STATUS_COLORS.error); }); } else { // This should never happen as coordinates are always captured before ping - debugError(`CRITICAL: No captured ping coordinates available for API post - this indicates a logic error`); - debugError(`Skipping API post to avoid posting incorrect coordinates`); + debugError(`[API QUEUE] CRITICAL: No captured ping coordinates available for API post - this indicates a logic error`); + debugError(`[API QUEUE] Skipping API post to avoid posting incorrect coordinates`); } // Clear timer reference @@ -3278,7 +3528,7 @@ async function sendPing(manual = false) { // Update distance display immediately after successful ping updateDistanceUi(); } catch (e) { - debugError(`Ping operation failed: ${e.message}`, e); + debugError(`[PING] Ping operation failed: ${e.message}`, e); setDynamicStatus(e.message || "Ping failed", STATUS_COLORS.error); // Unlock ping controls on error @@ -3288,17 +3538,17 @@ async function sendPing(manual = false) { // ---- Auto mode ---- function stopAutoPing(stopGps = false) { - debugLog(`stopAutoPing called (stopGps=${stopGps})`); + debugLog(`[AUTO] stopAutoPing called (stopGps=${stopGps})`); // Check if we're in cooldown before stopping (unless stopGps is true for disconnect) if (!stopGps && isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); - debugLog(`Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); + debugLog(`[AUTO] Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); setDynamicStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } if (state.autoTimerId) { - debugLog("Clearing auto ping timer"); + debugLog("[AUTO] Clearing auto ping timer"); clearTimeout(state.autoTimerId); state.autoTimerId = null; } @@ -3316,16 +3566,16 @@ function stopAutoPing(stopGps = false) { state.running = false; updateAutoButton(); releaseWakeLock(); - debugLog("Auto ping stopped"); + debugLog("[AUTO] Auto ping stopped"); } function scheduleNextAutoPing() { if (!state.running) { - debugLog("Not scheduling next auto ping - auto mode not running"); + debugLog("[AUTO] Not scheduling next auto ping - auto mode not running"); return; } const intervalMs = getSelectedIntervalMs(); - debugLog(`Scheduling next auto ping in ${intervalMs}ms`); + debugLog(`[AUTO] Scheduling next auto ping in ${intervalMs}ms`); // Start countdown immediately (skipReason may be set if ping was skipped) startAutoCountdown(intervalMs); @@ -3335,16 +3585,16 @@ function scheduleNextAutoPing() { if (state.running) { // Clear skip reason before next attempt state.skipReason = null; - debugLog("Auto ping timer fired, sending ping"); + debugLog("[AUTO] Auto ping timer fired, sending ping"); sendPing(false).catch(console.error); } }, intervalMs); } function startAutoPing() { - debugLog("startAutoPing called"); + debugLog("[AUTO] startAutoPing called"); if (!state.connection) { - debugError("Cannot start auto ping - not connected"); + debugError("[AUTO] Cannot start auto ping - not connected"); alert("Connect to a MeshCore device first."); return; } @@ -3352,14 +3602,14 @@ function startAutoPing() { // Check if we're in cooldown if (isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); - debugLog(`Auto ping start blocked by cooldown (${remainingSec}s remaining)`); + debugLog(`[AUTO] Auto ping start blocked by cooldown (${remainingSec}s remaining)`); setDynamicStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } // Clean up any existing auto-ping timer (but keep GPS watch running) if (state.autoTimerId) { - debugLog("Clearing existing auto ping timer"); + debugLog("[AUTO] Clearing existing auto ping timer"); clearTimeout(state.autoTimerId); state.autoTimerId = null; } @@ -3369,26 +3619,26 @@ function startAutoPing() { state.skipReason = null; // Start GPS watch for continuous updates - debugLog("Starting GPS watch for auto mode"); + debugLog("[AUTO] Starting GPS watch for auto mode"); startGeoWatch(); state.running = true; updateAutoButton(); // Acquire wake lock for auto mode - debugLog("Acquiring wake lock for auto mode"); + debugLog("[AUTO] Acquiring wake lock for auto mode"); acquireWakeLock().catch(console.error); // Send first ping immediately - debugLog("Sending initial auto ping"); + debugLog("[AUTO] Sending initial auto ping"); sendPing(false).catch(console.error); } // ---- BLE connect / disconnect ---- async function connect() { - debugLog("connect() called"); + debugLog("[BLE] connect() called"); if (!("bluetooth" in navigator)) { - debugError("Web Bluetooth not supported"); + debugError("[BLE] Web Bluetooth not supported"); alert("Web Bluetooth not supported in this browser."); return; } @@ -3399,62 +3649,62 @@ async function connect() { setDynamicStatus("Idle"); // Clear dynamic status try { - debugLog("Opening BLE connection..."); + debugLog("[BLE] Opening BLE connection..."); setDynamicStatus("BLE Connection Started", STATUS_COLORS.info); // Show BLE connection start const conn = await WebBleConnection.open(); state.connection = conn; - debugLog("BLE connection object created"); + debugLog("[BLE] BLE connection object created"); conn.on("connected", async () => { - debugLog("BLE connected event fired"); + debugLog("[BLE] BLE connected event fired"); // Keep "Connecting" status visible during the full connection process // Don't show "Connected" until everything is complete setConnectButton(true); connectBtn.disabled = false; const selfInfo = await conn.getSelfInfo(); - debugLog(`Device info: ${selfInfo?.name || "[No device]"}`); + debugLog(`[BLE] Device info: ${selfInfo?.name || "[No device]"}`); // Validate and store public key if (!selfInfo?.publicKey || selfInfo.publicKey.length !== 32) { - debugError("Missing or invalid public key from device", selfInfo?.publicKey); + debugError("[BLE] Missing or invalid public key from device", selfInfo?.publicKey); state.disconnectReason = "public_key_error"; // Mark specific disconnect reason // Disconnect after a brief delay to ensure "Acquiring wardriving slot" status is visible // before the disconnect sequence begins with "Disconnecting" setTimeout(() => { - disconnect().catch(err => debugError(`Disconnect after public key error failed: ${err.message}`)); + disconnect().catch(err => debugError(`[BLE] Disconnect after public key error failed: ${err.message}`)); }, 1500); return; } // Convert public key to hex and store state.devicePublicKey = BufferUtils.bytesToHex(selfInfo.publicKey); - debugLog(`Device public key stored: ${state.devicePublicKey.substring(0, 16)}...`); + debugLog(`[BLE] Device public key stored: ${state.devicePublicKey.substring(0, 16)}...`); deviceInfoEl.textContent = selfInfo?.name || "[No device]"; updateAutoButton(); try { await conn.syncDeviceTime?.(); - debugLog("Device time synced"); + debugLog("[BLE] Device time synced"); } catch { - debugLog("Device time sync not available or failed"); + debugLog("[BLE] Device time sync not available or failed"); } try { // Check capacity immediately after time sync, before channel setup and GPS init const allowed = await checkCapacity("connect"); if (!allowed) { - debugWarn("Capacity check denied, disconnecting"); + debugWarn("[CAPACITY] Capacity check denied, disconnecting"); // disconnectReason already set by checkCapacity() // Status message will be set by disconnected event handler based on disconnectReason // Disconnect after a brief delay to ensure "Acquiring wardriving slot" is visible setTimeout(() => { - disconnect().catch(err => debugError(`Disconnect after capacity denial failed: ${err.message}`)); + disconnect().catch(err => debugError(`[BLE] Disconnect after capacity denial failed: ${err.message}`)); }, 1500); return; } // Capacity check passed setDynamicStatus("Acquired wardriving slot", STATUS_COLORS.success); - debugLog("Wardriving slot acquired successfully"); + debugLog("[BLE] Wardriving slot acquired successfully"); // Proceed with channel setup and GPS initialization await ensureChannel(); @@ -3464,67 +3714,67 @@ async function connect() { // GPS initialization setDynamicStatus("Priming GPS", STATUS_COLORS.info); - debugLog("Starting GPS initialization"); + debugLog("[BLE] Starting GPS initialization"); await primeGpsOnce(); // Connection complete, show Connected status in connection bar setConnStatus("Connected", STATUS_COLORS.success); setDynamicStatus("Idle"); // Clear dynamic status to em dash - debugLog("Full connection process completed successfully"); + debugLog("[BLE] Full connection process completed successfully"); } catch (e) { - debugError(`Channel setup failed: ${e.message}`, e); + debugError(`[CHANNEL] Channel setup failed: ${e.message}`, e); state.disconnectReason = "channel_setup_error"; // Mark specific disconnect reason state.channelSetupErrorMessage = e.message || "Channel setup failed"; // Store error message } }); conn.on("disconnected", () => { - debugLog("BLE disconnected event fired"); - debugLog(`Disconnect reason: ${state.disconnectReason}`); + debugLog("[BLE] BLE disconnected event fired"); + debugLog(`[BLE] Disconnect reason: ${state.disconnectReason}`); // Always set connection bar to "Disconnected" setConnStatus("Disconnected", STATUS_COLORS.error); // Set dynamic status based on disconnect reason (WITHOUT "Disconnected:" prefix) if (state.disconnectReason === "capacity_full") { - debugLog("Branch: capacity_full"); + debugLog("[BLE] Branch: capacity_full"); setDynamicStatus("WarDriving app has reached capacity", STATUS_COLORS.error, true); - debugLog("Setting terminal status for capacity full"); + debugLog("[BLE] Setting terminal status for capacity full"); } else if (state.disconnectReason === "app_down") { - debugLog("Branch: app_down"); + debugLog("[BLE] Branch: app_down"); setDynamicStatus("WarDriving app is down", STATUS_COLORS.error, true); - debugLog("Setting terminal status for app down"); + debugLog("[BLE] Setting terminal status for app down"); } else if (state.disconnectReason === "slot_revoked") { - debugLog("Branch: slot_revoked"); + debugLog("[BLE] Branch: slot_revoked"); setDynamicStatus("WarDriving slot has been revoked", STATUS_COLORS.error, true); - debugLog("Setting terminal status for slot revocation"); + debugLog("[BLE] Setting terminal status for slot revocation"); } else if (state.disconnectReason === "session_id_error") { - debugLog("Branch: session_id_error"); + debugLog("[BLE] Branch: session_id_error"); setDynamicStatus("Session ID error; try reconnecting", STATUS_COLORS.error, true); - debugLog("Setting terminal status for session_id error"); + debugLog("[BLE] Setting terminal status for session_id error"); } else if (state.disconnectReason === "public_key_error") { - debugLog("Branch: public_key_error"); + debugLog("[BLE] Branch: public_key_error"); setDynamicStatus("Unable to read device public key; try again", STATUS_COLORS.error, true); - debugLog("Setting terminal status for public key error"); + debugLog("[BLE] Setting terminal status for public key error"); } else if (state.disconnectReason === "channel_setup_error") { - debugLog("Branch: channel_setup_error"); + debugLog("[BLE] Branch: channel_setup_error"); const errorMsg = state.channelSetupErrorMessage || "Channel setup failed"; setDynamicStatus(errorMsg, STATUS_COLORS.error, true); - debugLog("Setting terminal status for channel setup error"); + debugLog("[BLE] Setting terminal status for channel setup error"); state.channelSetupErrorMessage = null; // Clear after use (also cleared in cleanup as safety net) } else if (state.disconnectReason === "ble_disconnect_error") { - debugLog("Branch: ble_disconnect_error"); + debugLog("[BLE] Branch: ble_disconnect_error"); const errorMsg = state.bleDisconnectErrorMessage || "BLE disconnect failed"; setDynamicStatus(errorMsg, STATUS_COLORS.error, true); - debugLog("Setting terminal status for BLE disconnect error"); + debugLog("[BLE] Setting terminal status for BLE disconnect error"); state.bleDisconnectErrorMessage = null; // Clear after use (also cleared in cleanup as safety net) } else if (state.disconnectReason === "normal" || state.disconnectReason === null || state.disconnectReason === undefined) { - debugLog("Branch: normal/null/undefined"); + debugLog("[BLE] Branch: normal/null/undefined"); setDynamicStatus("Idle"); // Show em dash for normal disconnect } else { - debugLog(`Branch: else (unknown reason: ${state.disconnectReason})`); + debugLog(`[BLE] Branch: else (unknown reason: ${state.disconnectReason})`); // For unknown disconnect reasons, show em dash - debugLog(`Showing em dash for unknown reason: ${state.disconnectReason}`); + debugLog(`[BLE] Showing em dash for unknown reason: ${state.disconnectReason}`); setDynamicStatus("Idle"); } @@ -3557,29 +3807,29 @@ async function connect() { // Clear RX log entries on disconnect rxLogState.entries = []; - renderRxLogEntries(); + renderRxLogEntries(true); // Full render to show placeholder updateRxLogSummary(); - debugLog("RX log cleared on disconnect"); + debugLog("[BLE] RX log cleared on disconnect"); state.lastFix = null; state.lastSuccessfulPingLocation = null; state.gpsState = "idle"; updateGpsUi(); updateDistanceUi(); - debugLog("Disconnect cleanup complete"); + debugLog("[BLE] Disconnect cleanup complete"); }); } catch (e) { - debugError(`BLE connection failed: ${e.message}`, e); + debugError(`[BLE] BLE connection failed: ${e.message}`, e); setConnStatus("Disconnected", STATUS_COLORS.error); setDynamicStatus("Connection failed", STATUS_COLORS.error); connectBtn.disabled = false; } } async function disconnect() { - debugLog("disconnect() called"); + debugLog("[BLE] disconnect() called"); if (!state.connection) { - debugLog("No connection to disconnect"); + debugLog("[BLE] No connection to disconnect"); return; } @@ -3596,7 +3846,7 @@ async function disconnect() { // 1. CRITICAL: Flush API queue FIRST (session_id still valid) if (apiQueue.messages.length > 0) { - debugLog(`Flushing ${apiQueue.messages.length} queued messages before disconnect`); + debugLog(`[BLE] Flushing ${apiQueue.messages.length} queued messages before disconnect`); await flushApiQueue(); } stopFlushTimers(); @@ -3604,10 +3854,10 @@ async function disconnect() { // 2. THEN release capacity slot if we have a public key if (state.devicePublicKey) { try { - debugLog("Releasing capacity slot"); + debugLog("[BLE] Releasing capacity slot"); await checkCapacity("disconnect"); } catch (e) { - debugWarn(`Failed to release capacity slot: ${e.message}`); + debugWarn(`[CAPACITY] Failed to release capacity slot: ${e.message}`); // Don't fail disconnect if capacity release fails } } @@ -3615,12 +3865,12 @@ async function disconnect() { // 3. Delete the wardriving channel before disconnecting try { if (state.channel && typeof state.connection.deleteChannel === "function") { - debugLog(`Deleting channel ${CHANNEL_NAME} at index ${state.channel.channelIdx}`); + debugLog(`[BLE] Deleting channel ${CHANNEL_NAME} at index ${state.channel.channelIdx}`); await state.connection.deleteChannel(state.channel.channelIdx); - debugLog(`Channel ${CHANNEL_NAME} deleted successfully`); + debugLog(`[BLE] Channel ${CHANNEL_NAME} deleted successfully`); } } catch (e) { - debugWarn(`Failed to delete channel ${CHANNEL_NAME}: ${e.message}`); + debugWarn(`[CHANNEL] Failed to delete channel ${CHANNEL_NAME}: ${e.message}`); // Don't fail disconnect if channel deletion fails } @@ -3628,19 +3878,19 @@ async function disconnect() { try { // WebBleConnection typically exposes one of these. if (typeof state.connection.close === "function") { - debugLog("Calling connection.close()"); + debugLog("[BLE] Calling connection.close()"); await state.connection.close(); } else if (typeof state.connection.disconnect === "function") { - debugLog("Calling connection.disconnect()"); + debugLog("[BLE] Calling connection.disconnect()"); await state.connection.disconnect(); } else if (typeof state.connection.device?.gatt?.disconnect === "function") { - debugLog("Calling device.gatt.disconnect()"); + debugLog("[BLE] Calling device.gatt.disconnect()"); state.connection.device.gatt.disconnect(); } else { - debugWarn("No known disconnect method on connection object"); + debugWarn("[BLE] No known disconnect method on connection object"); } } catch (e) { - debugError(`BLE disconnect failed: ${e.message}`, e); + debugError(`[BLE] BLE disconnect failed: ${e.message}`, e); state.disconnectReason = "ble_disconnect_error"; // Mark specific disconnect reason state.bleDisconnectErrorMessage = e.message || "Disconnect failed"; // Store error message } finally { @@ -3652,17 +3902,17 @@ async function disconnect() { // ---- Page visibility ---- document.addEventListener("visibilitychange", async () => { if (document.hidden) { - debugLog("Page visibility changed to hidden"); + debugLog("[UI] Page visibility changed to hidden"); if (state.running) { - debugLog("Stopping auto ping due to page hidden"); + debugLog("[UI] Stopping auto ping due to page hidden"); stopAutoPing(true); // Ignore cooldown check when page is hidden setDynamicStatus("Lost focus, auto mode stopped", STATUS_COLORS.warning); } else { - debugLog("Releasing wake lock due to page hidden"); + debugLog("[UI] Releasing wake lock due to page hidden"); releaseWakeLock(); } } else { - debugLog("Page visibility changed to visible"); + debugLog("[UI] Page visibility changed to visible"); // On visible again, user can manually re-start Auto. } }); @@ -3680,10 +3930,10 @@ function updateConnectButtonState() { // Update dynamic status based on power selection if (!radioPowerSelected) { - debugLog("Radio power not selected - showing message in status bar"); + debugLog("[UI] Radio power not selected - showing message in status bar"); setDynamicStatus("Select radio power to connect", STATUS_COLORS.warning); } else { - debugLog("Radio power selected - clearing message from status bar"); + debugLog("[UI] Radio power selected - clearing message from status bar"); setDynamicStatus("Idle"); } } @@ -3691,7 +3941,7 @@ function updateConnectButtonState() { // ---- Bind UI & init ---- export async function onLoad() { - debugLog("wardrive.js onLoad() called - initializing"); + debugLog("[INIT] wardrive.js onLoad() called - initializing"); setConnStatus("Disconnected", STATUS_COLORS.error); enableControls(false); updateAutoButton(); @@ -3707,16 +3957,16 @@ export async function onLoad() { await connect(); } } catch (e) { - debugError(`Connection button error: ${e.message}`, e); + debugError("[UI] Connection button error:" ${e.message}`, e); setDynamicStatus(e.message || "Connection failed", STATUS_COLORS.error); } }); sendPingBtn.addEventListener("click", () => { - debugLog("Manual ping button clicked"); + debugLog("[UI] Manual ping button clicked"); sendPing(true).catch(console.error); }); autoToggleBtn.addEventListener("click", () => { - debugLog("Auto toggle button clicked"); + debugLog("[UI] Auto toggle button clicked"); if (state.running) { stopAutoPing(); setDynamicStatus("Auto mode stopped", STATUS_COLORS.idle); @@ -3733,7 +3983,7 @@ export async function onLoad() { if (settingsGearBtn && settingsPanel && connectionBar) { settingsGearBtn.addEventListener("click", () => { - debugLog("Settings gear button clicked"); + debugLog("[UI] Settings gear button clicked"); const isHidden = settingsPanel.classList.contains("hidden"); settingsPanel.classList.toggle("hidden"); @@ -3752,7 +4002,7 @@ export async function onLoad() { if (settingsCloseBtn && settingsPanel && connectionBar) { settingsCloseBtn.addEventListener("click", () => { - debugLog("Settings close button clicked"); + debugLog("[UI] Settings close button clicked"); settingsPanel.classList. add("hidden"); // Restore full rounded corners to connection bar connectionBar.classList.remove("rounded-t-xl", "rounded-b-none"); @@ -3764,7 +4014,7 @@ export async function onLoad() { const powerRadios = document.querySelectorAll('input[name="power"]'); powerRadios.forEach(radio => { radio.addEventListener("change", () => { - debugLog(`Radio power changed to: ${getCurrentPowerSetting()}`); + debugLog(`[UI] Radio power changed to: ${getCurrentPowerSetting()}`); updateConnectButtonState(); }); }); @@ -3772,7 +4022,7 @@ export async function onLoad() { // Session Log event listener if (logSummaryBar) { logSummaryBar.addEventListener("click", () => { - debugLog("Log summary bar clicked - toggling session log"); + debugLog("[UI] Log summary bar clicked - toggling session log"); toggleBottomSheet(); }); } @@ -3785,13 +4035,21 @@ export async function onLoad() { }); } + // Error Log event listener + if (errorLogSummaryBar) { + errorLogSummaryBar.addEventListener("click", () => { + debugLog("[ERROR LOG] Error log summary bar clicked - toggling Error log"); + toggleErrorLogBottomSheet(); + }); + } + // Prompt location permission early (optional) - debugLog("Requesting initial location permission"); + debugLog("[GPS] Requesting initial location permission"); try { await getCurrentPosition(); - debugLog("Initial location permission granted"); + debugLog("[GPS] Initial location permission granted"); } catch (e) { - debugLog(`Initial location permission not granted: ${e.message}`); + debugLog(`[GPS] Initial location permission not granted: ${e.message}`); } - debugLog("wardrive.js initialization complete"); + debugLog("[INIT] wardrive.js initialization complete"); } diff --git a/docs/DEVELOPMENT_REQUIREMENTS.md b/docs/DEVELOPMENT_REQUIREMENTS.md index bd2a592..a3a83c7 100644 --- a/docs/DEVELOPMENT_REQUIREMENTS.md +++ b/docs/DEVELOPMENT_REQUIREMENTS.md @@ -16,6 +16,48 @@ This document defines the coding standards and requirements for all changes to t - Debug logging is controlled by the `DEBUG_ENABLED` flag (URL parameter `? debug=true`) - Log at key points: function entry, API calls, state changes, errors, and decision branches +#### Debug Log Tagging Convention + +All debug log messages **MUST** include a descriptive tag in square brackets immediately after `[DEBUG]` that identifies the subsystem or feature area. This enables easier filtering and understanding of debug output. + +**Format:** `[DEBUG] [TAG] Message here` + +**Required Tags:** + +| Tag | Description | +|-----|-------------| +| `[BLE]` | Bluetooth connection and device communication | +| `[GPS]` | GPS/geolocation operations | +| `[PING]` | Ping sending and validation | +| `[API QUEUE]` | API batch queue operations | +| `[RX BATCH]` | RX batch buffer operations | +| `[PASSIVE RX]` | Passive RX logging logic | +| `[PASSIVE RX UI]` | Passive RX UI rendering | +| `[SESSION LOG]` | Session log tracking | +| `[UNIFIED RX]` | Unified RX handler | +| `[DECRYPT]` | Message decryption | +| `[UI]` | General UI updates (status bar, buttons, etc.) | +| `[CHANNEL]` | Channel setup and management | +| `[TIMER]` | Timer and countdown operations | +| `[WAKE LOCK]` | Wake lock acquisition/release | +| `[GEOFENCE]` | Geofence and distance validation | +| `[CAPACITY]` | Capacity check API calls | +| `[AUTO]` | Auto ping mode operations | +| `[INIT]` | Initialization and setup | +| `[ERROR LOG]` | Error log UI operations | + +**Examples:** +```javascript +// ✅ Correct - includes tag +debugLog("[BLE] Connection established"); +debugLog("[GPS] Fresh position acquired: lat=45.12345, lon=-75.12345"); +debugLog("[PING] Sending ping to channel 2"); + +// ❌ Incorrect - missing tag +debugLog("Connection established"); +debugLog("Fresh position acquired"); +``` + ### Status Messages - **ALWAYS** update `STATUS_MESSAGES.md` when adding or modifying user-facing status messages - Use the `setStatus(message, color)` function for all UI status updates diff --git a/index.html b/index.html index e31f6e6..ac08c89 100644 --- a/index.html +++ b/index.html @@ -251,6 +251,32 @@

+
+
+

Error Log

+ | + 0 errors + +
+
+ + + + +
+
+ + +