From 970277717bdb9054884d8696989d15715489d989 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Sun, 21 Dec 2025 20:13:00 -0500 Subject: [PATCH 01/77] feat: add coverage block definitions for mesh network signal quality --- docs/COVERAGE_TYPES.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/COVERAGE_TYPES.md diff --git a/docs/COVERAGE_TYPES.md b/docs/COVERAGE_TYPES.md new file mode 100644 index 0000000..38ea3f1 --- /dev/null +++ b/docs/COVERAGE_TYPES.md @@ -0,0 +1,20 @@ +# 📡 Coverage Block Definitions + +Coverage blocks are used to classify mesh network signal quality during wardriving sessions. Each block represents a specific communication state between your device and the mesh, based on whether transmissions were sent, received, repeated, or successfully routed. Use these definitions to interpret your wardrive map data. + +--- + +## BIDIRECTIONAL (BIDIR) +Heard repeats from the mesh **AND** successfully routed through it. Full bidirectional communication confirmed. + +## TRANSMIT (TX) +Successfully routed through the mesh, but did **NOT** hear any repeats back. Outbound path confirmed, inbound uncertain. + +## RECEIVE (RX) +Heard mesh traffic while wardriving, but did **NOT** transmit. Passive reception only. + +## DEAD +A repeater heard the transmission, but either did nothing with it or when it repeated, no other radio received it. Message never made it through the mesh. + +## DROP +No repeats heard **AND** no successful route through the mesh. No communication in either direction. \ No newline at end of file From 79a7e589cc8cedc859c79e754ec104bb70875202 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Sun, 21 Dec 2025 22:11:00 -0500 Subject: [PATCH 02/77] docs: add color definitions to coverage block types in COVERAGE_TYPES.md --- docs/COVERAGE_TYPES.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/COVERAGE_TYPES.md b/docs/COVERAGE_TYPES.md index 38ea3f1..c827af2 100644 --- a/docs/COVERAGE_TYPES.md +++ b/docs/COVERAGE_TYPES.md @@ -4,17 +4,17 @@ Coverage blocks are used to classify mesh network signal quality during wardrivi --- -## BIDIRECTIONAL (BIDIR) +## BIDIRECTIONAL (BIDIR) - Green Heard repeats from the mesh **AND** successfully routed through it. Full bidirectional communication confirmed. -## TRANSMIT (TX) +## TRANSMIT (TX) - Orange Successfully routed through the mesh, but did **NOT** hear any repeats back. Outbound path confirmed, inbound uncertain. -## RECEIVE (RX) +## RECEIVE (RX) - Orange Heard mesh traffic while wardriving, but did **NOT** transmit. Passive reception only. -## DEAD +## DEAD - Grey A repeater heard the transmission, but either did nothing with it or when it repeated, no other radio received it. Message never made it through the mesh. -## DROP +## DROP - Red No repeats heard **AND** no successful route through the mesh. No communication in either direction. \ No newline at end of file From d28f350d0518ac5880b933a86195e035e57cb9fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 03:50:05 +0000 Subject: [PATCH 03/77] Initial plan From 01788baaa229b0fbd73c0848cbe01e55047f9f19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 03:57:13 +0000 Subject: [PATCH 04/77] Implement passive RX log listening feature - Added rxLogState object and passiveRxTracking state variables - Created handlePassiveRxLogEvent() to parse all incoming packets - Implemented startPassiveRxListening() and stopPassiveRxListening() - Integrated passive listening into connected/disconnected handlers - Added RX Log UI section in index.html - Implemented all RX Log UI helper functions - Added future API integration placeholder - Added comprehensive debug logging throughout Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/tailwind.css | 13 ++ content/wardrive.js | 343 +++++++++++++++++++++++++++++++++++++++++++ index.html | 26 ++++ 3 files changed, 382 insertions(+) diff --git a/content/tailwind.css b/content/tailwind.css index bdfe2d6..3e887f1 100644 --- a/content/tailwind.css +++ b/content/tailwind.css @@ -433,6 +433,9 @@ .rounded { border-radius: 0.25rem; } + .rounded-full { + border-radius: calc(infinity * 1px); + } .rounded-lg { border-radius: var(--radius-lg); } @@ -721,6 +724,16 @@ } } } + .hover\:bg-slate-700\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-700) 50%, transparent); + } + } + } + } .hover\:bg-slate-800\/60 { &:hover { @media (hover: hover) { diff --git a/content/wardrive.js b/content/wardrive.js index 34a1983..17c4318 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -78,6 +78,7 @@ const MESHMAPPER_API_URL = "https://yow.meshmapper.net/wardriving-api.php"; const MESHMAPPER_CAPACITY_CHECK_URL = "https://yow.meshmapper.net/capacitycheck.php"; const MESHMAPPER_API_KEY = "59C7754DABDF5C11CA5F5D8368F89"; const MESHMAPPER_DEFAULT_WHO = "GOME-WarDriver"; // Default identifier +const MESHMAPPER_RX_LOG_API_URL = null; // TODO: Set when API endpoint is ready // Static for now; will be made dynamic later. const WARDIVE_IATA_CODE = "YOW"; @@ -127,6 +128,16 @@ const logCount = $("logCount"); const logLastTime = $("logLastTime"); const logLastSnr = $("logLastSnr"); +// RX Log selectors +const rxLogSummaryBar = $("rxLogSummaryBar"); +const rxLogBottomSheet = $("rxLogBottomSheet"); +const rxLogScrollContainer = $("rxLogScrollContainer"); +const rxLogCount = $("rxLogCount"); +const rxLogLastTime = $("rxLogLastTime"); +const rxLogLastRepeater = $("rxLogLastRepeater"); +const rxLogEntries = $("rxLogEntries"); +const rxLogExpandArrow = $("rxLogExpandArrow"); + // Session log state const sessionLogState = { entries: [], // Array of parsed log entries @@ -134,6 +145,14 @@ const sessionLogState = { autoScroll: true }; +// RX log state (passive observations) +const rxLogState = { + entries: [], // Array of parsed RX log entries + isExpanded: false, + autoScroll: true, + maxEntries: 100 // Limit to prevent memory issues +}; + // ---- State ---- const state = { connection: null, @@ -171,6 +190,11 @@ const state = { listenTimeout: null, // Timeout handle for 7-second window rxLogHandler: null, // Handler function for rx_log events currentLogEntry: null, // Current log entry being updated (for incremental UI updates) + }, + passiveRxTracking: { + isListening: false, // Whether we're currently listening passively + rxLogHandler: null, // Handler function for passive rx_log events + entries: [] // Array of { repeaterId, snr, lat, lon, timestamp } } }; @@ -1752,6 +1776,138 @@ function formatRepeaterTelemetry(repeaters) { return repeaters.map(r => `${r.repeaterId}(${r.snr})`).join(','); } +// ---- Passive RX Log Listening ---- + +/** + * Handle passive RX log event - monitors all incoming packets + * Extracts the LAST hop from the path (direct repeater) and records observation + * @param {Object} data - The LogRxData event data (contains lastSnr, lastRssi, raw) + */ +async function handlePassiveRxLogEvent(data) { + try { + debugLog(`[PASSIVE RX] Received rx_log entry: SNR=${data.lastSnr}, RSSI=${data.lastRssi}`); + + // Parse the packet from raw data + const packet = Packet.fromBytes(data.raw); + + // VALIDATION STEP 1: Header validation + // Expected header for channel GroupText packets: 0x15 + const EXPECTED_HEADER = 0x15; + if (packet.header !== EXPECTED_HEADER) { + debugLog(`[PASSIVE RX] Ignoring: header validation failed (header=0x${packet.header.toString(16).padStart(2, '0')})`); + return; + } + + debugLog(`[PASSIVE RX] Header validation passed: 0x${packet.header.toString(16).padStart(2, '0')}`); + + // VALIDATION STEP 2: Verify this message is for our wardriving channel + if (packet.payload.length < 3) { + debugLog(`[PASSIVE RX] Ignoring: payload too short`); + return; + } + + const packetChannelHash = packet.payload[0]; + if (WARDRIVING_CHANNEL_HASH !== null && packetChannelHash !== WARDRIVING_CHANNEL_HASH) { + debugLog(`[PASSIVE RX] Ignoring: channel hash mismatch (packet=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')})`); + return; + } + + debugLog(`[PASSIVE RX] Channel hash match confirmed`); + + // VALIDATION STEP 3: Check path length (need at least one hop) + if (packet.path.length === 0) { + debugLog(`[PASSIVE RX] Ignoring: no path (direct transmission, not via repeater)`); + return; + } + + // Extract LAST hop from path (the repeater that directly delivered to us) + const lastHopId = packet.path[packet.path.length - 1]; + const repeaterId = lastHopId.toString(16).padStart(2, '0'); + + debugLog(`[PASSIVE RX] Packet heard via last hop: ${repeaterId}, SNR=${data.lastSnr}, path_length=${packet.path.length}`); + + // Get current GPS location + if (!state.lastFix) { + debugLog(`[PASSIVE RX] No GPS fix available, skipping entry`); + return; + } + + const lat = state.lastFix.lat; + const lon = state.lastFix.lon; + const timestamp = new Date().toISOString(); + + // Add entry to RX log + addRxLogEntry(repeaterId, data.lastSnr, lat, lon, timestamp); + + debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${data.lastSnr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); + + } catch (error) { + debugError(`[PASSIVE RX] Error processing rx_log entry: ${error.message}`, error); + } +} + +/** + * Start passive RX listening for background repeater observations + */ +function startPassiveRxListening() { + if (state.passiveRxTracking.isListening) { + debugLog(`[PASSIVE RX] Already listening, skipping start`); + return; + } + + if (!state.connection) { + debugWarn(`[PASSIVE RX] Cannot start listening: no connection`); + return; + } + + debugLog(`[PASSIVE RX] Starting passive RX listening`); + + const handler = (data) => handlePassiveRxLogEvent(data); + state.passiveRxTracking.rxLogHandler = handler; + state.connection.on(Constants.PushCodes.LogRxData, handler); + state.passiveRxTracking.isListening = true; + + debugLog(`[PASSIVE RX] ✅ Passive listening started successfully`); +} + +/** + * Stop passive RX listening + */ +function stopPassiveRxListening() { + if (!state.passiveRxTracking.isListening) { + return; + } + + debugLog(`[PASSIVE RX] Stopping passive RX listening`); + + if (state.connection && state.passiveRxTracking.rxLogHandler) { + state.connection.off(Constants.PushCodes.LogRxData, state.passiveRxTracking.rxLogHandler); + debugLog(`[PASSIVE RX] Unregistered LogRxData event handler`); + } + + state.passiveRxTracking.isListening = false; + state.passiveRxTracking.rxLogHandler = null; + + debugLog(`[PASSIVE RX] ✅ Passive listening stopped`); +} + +/** + * Future: Post RX log data to MeshMapper API + * @param {Array} entries - Array of RX log entries + */ +async function postRxLogToMeshMapperAPI(entries) { + if (!MESHMAPPER_RX_LOG_API_URL) { + debugLog('[PASSIVE RX] RX Log API posting not configured yet'); + return; + } + + // Future implementation: + // - Batch post accumulated RX log entries + // - Include session_id from state.wardriveSessionId + // - Format: { observations: [{ repeaterId, snr, lat, lon, timestamp }] } + debugLog(`[PASSIVE RX] Would post ${entries.length} RX log entries to API (not implemented yet)`); +} + // ---- Mobile Session Log Bottom Sheet ---- /** @@ -2001,6 +2157,181 @@ function addLogEntry(timestamp, lat, lon, eventsStr) { } } +// ---- RX Log UI Functions ---- + +/** + * Parse RX log entry into structured data + * @param {Object} entry - RX log entry object + * @returns {Object} Parsed RX log entry with formatted data + */ +function parseRxLogEntry(entry) { + return { + repeaterId: entry.repeaterId, + snr: entry.snr, + lat: entry.lat.toFixed(5), + lon: entry.lon.toFixed(5), + timestamp: entry.timestamp + }; +} + +/** + * Create DOM element for RX log entry + * @param {Object} entry - RX log entry object + * @returns {HTMLElement} DOM element for the RX log entry + */ +function createRxLogEntryElement(entry) { + const parsed = parseRxLogEntry(entry); + + const logEntry = document.createElement('div'); + logEntry.className = 'logEntry'; + + // Top row: time + coords + const topRow = document.createElement('div'); + topRow.className = 'logRowTop'; + + const time = document.createElement('span'); + time.className = 'logTime'; + const date = new Date(parsed.timestamp); + time.textContent = date.toLocaleTimeString(); + + const coords = document.createElement('span'); + coords.className = 'logCoords'; + coords.textContent = `${parsed.lat},${parsed.lon}`; + + topRow.appendChild(time); + topRow.appendChild(coords); + + // Chips row: repeater ID and SNR + const chipsRow = document.createElement('div'); + chipsRow.className = 'heardChips'; + + // Create chip for repeater with SNR + const chip = createChipElement(parsed.repeaterId, parsed.snr); + chipsRow.appendChild(chip); + + logEntry.appendChild(topRow); + logEntry.appendChild(chipsRow); + + return logEntry; +} + +/** + * Update RX log summary bar with latest data + */ +function updateRxLogSummary() { + if (!rxLogCount || !rxLogLastTime || !rxLogLastRepeater) return; + + const count = rxLogState.entries.length; + rxLogCount.textContent = count === 1 ? '1 observation' : `${count} observations`; + + if (count === 0) { + rxLogLastTime.textContent = 'No data'; + rxLogLastRepeater.textContent = '—'; + debugLog('[PASSIVE RX UI] Summary updated: no entries'); + return; + } + + const lastEntry = rxLogState.entries[count - 1]; + const date = new Date(lastEntry.timestamp); + rxLogLastTime.textContent = date.toLocaleTimeString(); + rxLogLastRepeater.textContent = lastEntry.repeaterId; + + debugLog(`[PASSIVE RX UI] Summary updated: ${count} observations, last repeater: ${lastEntry.repeaterId}`); +} + +/** + * Render all RX log entries + */ +function renderRxLogEntries() { + 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; + } + + // 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`); +} + +/** + * Toggle RX log expanded/collapsed + */ +function toggleRxLogBottomSheet() { + rxLogState.isExpanded = !rxLogState.isExpanded; + + if (rxLogBottomSheet) { + if (rxLogState.isExpanded) { + rxLogBottomSheet.classList.add('open'); + rxLogBottomSheet.classList.remove('hidden'); + } else { + rxLogBottomSheet.classList.remove('open'); + rxLogBottomSheet.classList.add('hidden'); + } + } + + // Toggle arrow rotation + if (rxLogExpandArrow) { + if (rxLogState.isExpanded) { + rxLogExpandArrow.classList.add('expanded'); + } else { + rxLogExpandArrow.classList.remove('expanded'); + } + } +} + +/** + * Add entry to RX log + * @param {string} repeaterId - Repeater ID (hex) + * @param {number} snr - Signal-to-noise ratio + * @param {number} lat - Latitude + * @param {number} lon - Longitude + * @param {string} timestamp - ISO timestamp + */ +function addRxLogEntry(repeaterId, snr, lat, lon, timestamp) { + const entry = { + repeaterId, + snr, + lat, + lon, + timestamp + }; + + rxLogState.entries.push(entry); + + // Apply max entries limit + 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})`); + } + + renderRxLogEntries(); + updateRxLogSummary(); + + debugLog(`[PASSIVE RX UI] Added entry: repeater=${repeaterId}, snr=${snr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); +} + // ---- Ping ---- /** * Acquire fresh GPS coordinates and update state @@ -2576,6 +2907,9 @@ async function connect() { // Proceed with channel setup and GPS initialization await ensureChannel(); + // Start passive RX listening after channel setup + startPassiveRxListening(); + // GPS initialization setDynamicStatus("Priming GPS", STATUS_COLORS.info); debugLog("Starting GPS initialization"); @@ -2658,6 +2992,7 @@ async function connect() { stopGpsAgeUpdater(); // Ensure age updater stops stopDistanceUpdater(); // Ensure distance updater stops stopRepeaterTracking(); // Stop repeater echo tracking + stopPassiveRxListening(); // Stop passive RX listening // Clean up all timers cleanupAllTimers(); @@ -2870,6 +3205,14 @@ export async function onLoad() { }); } + // RX Log event listener + if (rxLogSummaryBar) { + rxLogSummaryBar.addEventListener("click", () => { + debugLog("[PASSIVE RX UI] RX log summary bar clicked - toggling RX log"); + toggleRxLogBottomSheet(); + }); + } + // Prompt location permission early (optional) debugLog("Requesting initial location permission"); try { diff --git a/index.html b/index.html index 5723b2f..e31f6e6 100644 --- a/index.html +++ b/index.html @@ -225,6 +225,32 @@

+
+
+

RX Log

+ | + 0 observations + +
+
+ + + + +
+
+ + +
From ce2e235be3c7f4085a3fcc971f8900e81cf2d56f Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Sun, 21 Dec 2025 22:59:02 -0500 Subject: [PATCH 05/77] chore: add placeholder for requested changes in development requirements --- docs/DEVELOPMENT_REQUIREMENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/DEVELOPMENT_REQUIREMENTS.md b/docs/DEVELOPMENT_REQUIREMENTS.md index cf53504..15cdf67 100644 --- a/docs/DEVELOPMENT_REQUIREMENTS.md +++ b/docs/DEVELOPMENT_REQUIREMENTS.md @@ -74,3 +74,4 @@ Below is the **current** app connection flow used when a user connects to a devi ### Requested Change +<< Requested Changes go here >> \ No newline at end of file From eddf1a28b236dfa8e53d0166332df83062a62058 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 03:59:17 +0000 Subject: [PATCH 06/77] Update CONNECTION_WORKFLOW.md for passive RX listening - Added step 9 for passive RX listening in connection workflow - Updated high-level connection steps to include passive listening - Added passive RX stop in disconnection cleanup - Added comprehensive "Passive RX Log Listening" section - Documented key differences between active ping tracking and passive RX - Explained first hop vs last hop path interpretation - Documented UI components and future API integration - Updated summary with new design principle Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- docs/CONNECTION_WORKFLOW.md | 142 +++++++++++++++++++++++++++++++----- 1 file changed, 125 insertions(+), 17 deletions(-) diff --git a/docs/CONNECTION_WORKFLOW.md b/docs/CONNECTION_WORKFLOW.md index 2fa004e..a4a340c 100644 --- a/docs/CONNECTION_WORKFLOW.md +++ b/docs/CONNECTION_WORKFLOW.md @@ -62,8 +62,9 @@ 6. **Time Sync** → Synchronizes device clock 7. **Capacity Check** → Acquires API slot from MeshMapper 8. **Channel Setup** → Creates/finds #wardriving channel -9. **GPS Init** → Starts GPS tracking -10. **Connected** → Enables all controls, ready for wardriving +9. **Passive RX Listening** → Starts background packet monitoring +10. **GPS Init** → Starts GPS tracking +11. **Connected** → Enables all controls, ready for wardriving ### Detailed Connection Steps @@ -173,22 +174,34 @@ connectBtn.addEventListener("click", async () => { - Stores channel object in `state.channel` - Updates UI: "#wardriving (CH:X)" -9. **Initialize GPS** +9. **Start Passive RX Listening** - **Connection Status**: `"Connecting"` (blue, maintained) - - **Dynamic Status**: `"Priming GPS"` (blue) - - Requests location permission - - Gets initial GPS position (30s timeout) - - Starts continuous GPS watch - - Starts GPS age updater (1s interval) - - Starts distance updater (3s interval) - - Updates UI with coordinates and accuracy - - Refreshes coverage map if accuracy < 100m - -10. **Connection Complete** + - **Dynamic Status**: No user-facing message (background operation) + - Registers event handler for `LogRxData` events + - Begins monitoring all incoming packets on wardriving channel + - Extracts last hop (direct repeater) from each packet + - Records observations: repeater ID, SNR, GPS location, timestamp + - Populates RX Log UI with real-time observations + - Operates independently of active ping operations + - **Debug Logging**: `[PASSIVE RX]` prefix for all debug messages + +10. **Initialize GPS** + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"Priming GPS"` (blue) + - Requests location permission + - Gets initial GPS position (30s timeout) + - Starts continuous GPS watch + - Starts GPS age updater (1s interval) + - Starts distance updater (3s interval) + - Updates UI with coordinates and accuracy + - Refreshes coverage map if accuracy < 100m + +11. **Connection Complete** - **Connection Status**: `"Connected"` (green) - **NOW shown after GPS init** - **Dynamic Status**: `"—"` (em dash - cleared to show empty state) - Enables all UI controls - Ready for wardriving operations + - Passive RX listening running in background ## Disconnection Workflow @@ -262,6 +275,7 @@ See `content/wardrive.js` lines 2119-2179 for the main `disconnect()` function. - Stops GPS age updater - Stops distance updater - Stops repeater tracking + - **Stops passive RX listening** (unregisters LogRxData handler) - Clears all timers (see `cleanupAllTimers()`) - Releases wake lock - Clears connection state @@ -576,6 +590,99 @@ stateDiagram-v2 - Error → Disconnected - Recovery always possible +## Passive RX Log Listening + +### Overview + +The passive RX log listening feature monitors all incoming packets on the wardriving channel without adding any traffic to the mesh network. This provides visibility into which repeaters can be heard at the current GPS location. + +### Key Differences: Active Ping Tracking vs Passive RX Listening + +**Active Ping Tracking (Existing):** +- Triggered when user sends a ping +- Validates incoming packets are echoes of our specific ping message +- Extracts **first hop** (first repeater in the path) +- Tracks repeaters that first forwarded our message into the mesh +- Runs for 7 seconds after each ping +- Results shown in Session Log + +**Passive RX Listening (New):** +- Runs continuously in background once connected +- Monitors **all** packets on wardriving channel (not just our pings) +- Extracts **last hop** (repeater that directly delivered packet to us) +- Shows which repeaters we can actually hear from current location +- No time limit - runs entire connection duration +- Results shown in RX Log UI section + +### Path Interpretation + +For a packet with path: `77 → 92 → 0C` +- **First hop (ping tracking)**: `77` - origin repeater that first flooded our message +- **Last hop (passive RX)**: `0C` - repeater that directly delivered the packet to us + +The last hop is more relevant for coverage mapping because it represents the repeater we can actually receive signals from at our current GPS coordinates. + +### Implementation Details + +**Startup:** +1. Connection established +2. Channel setup completes +3. `startPassiveRxListening()` called +4. Registers handler for `Constants.PushCodes.LogRxData` events +5. Handler: `handlePassiveRxLogEvent()` + +**Packet Processing:** +1. Parse packet from raw bytes +2. Validate header (0x15 - GroupText/Flood) +3. Validate channel hash matches wardriving channel +4. Check path length (skip if no repeaters) +5. Extract last hop from path +6. Get current GPS coordinates +7. Record observation: `{repeaterId, snr, lat, lon, timestamp}` +8. Update RX Log UI + +**Shutdown:** +1. Disconnect initiated +2. `stopPassiveRxListening()` called in disconnect cleanup +3. Unregisters LogRxData event handler +4. Clears state + +### UI Components + +**RX Log Section** (below Session Log): +- Header bar showing observation count and last repeater +- Expandable/collapsible panel +- Scrollable list of observations (newest first) +- Each entry shows: timestamp, GPS coords, repeater ID, SNR chip +- Max 100 entries (oldest removed when limit reached) + +### Future API Integration + +Placeholder function `postRxLogToMeshMapperAPI()` ready for future implementation: +- Batch post accumulated observations +- Include session_id from capacity check +- Format: `{observations: [{repeaterId, snr, lat, lon, timestamp}]}` +- API endpoint: `MESHMAPPER_RX_LOG_API_URL` (currently null) + +### Debug Logging + +All passive RX operations use `[PASSIVE RX]` prefix: +- `[PASSIVE RX] Starting passive RX listening` +- `[PASSIVE RX] Received rx_log entry: SNR=X` +- `[PASSIVE RX] Header validation passed` +- `[PASSIVE RX] Observation logged: repeater=XX` +- `[PASSIVE RX UI] Summary updated: N observations` + +Enable debug mode with URL parameter: `?debug=true` + +### Coexistence with Active Ping Tracking + +Both handlers listen to the same `LogRxData` event simultaneously: +- **Active handler**: Validates message content matches our ping, extracts first hop +- **Passive handler**: Processes all messages, extracts last hop +- No conflicts - they serve different purposes and operate independently +- Event system supports multiple handlers on the same event + ## Summary MeshCore-GOME-WarDriver implements a robust Web Bluetooth wardriving application with clear connection/disconnection workflows: @@ -586,11 +693,12 @@ MeshCore-GOME-WarDriver implements a robust Web Bluetooth wardriving application 3. **Comprehensive Cleanup**: All resources explicitly released 4. **Clear State Machine**: No ambiguous states 5. **User Transparency**: Status messages at every step +6. **Passive Background Monitoring**: Continuous RX logging without mesh traffic -**Connection:** BLE → Device Info → Time Sync → Capacity Check → Channel Setup → GPS → Connected +**Connection:** BLE → Device Info → Time Sync → Capacity Check → Channel Setup → Passive RX Start → GPS → Connected -**Disconnection:** Capacity Release → Channel Delete → BLE Close → Full Cleanup → Disconnected +**Disconnection:** Capacity Release → Channel Delete → BLE Close → Full Cleanup (including Passive RX Stop) → Disconnected -**Debug Mode:** Add `?debug=true` to URL for detailed logging +**Debug Mode:** Add `?debug=true` to URL for detailed logging (including `[PASSIVE RX]` messages) -The workflow prioritizes reliability, clear error messages, and complete resource cleanup on every disconnect. +The workflow prioritizes reliability, clear error messages, complete resource cleanup on every disconnect, and non-intrusive background observation of mesh network activity. From e2b0db53de53478dfa6eaf1c78a1266530281510 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:11:28 +0000 Subject: [PATCH 07/77] Initial plan From 068b8f57aa65d665a85c23e1635af62d8ee22104 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:13:28 +0000 Subject: [PATCH 08/77] Comment out channel hash filter in handlePassiveRxLogEvent to track all RX messages Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 17c4318..1eb77f0 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -1806,13 +1806,15 @@ async function handlePassiveRxLogEvent(data) { return; } - const packetChannelHash = packet.payload[0]; - if (WARDRIVING_CHANNEL_HASH !== null && packetChannelHash !== WARDRIVING_CHANNEL_HASH) { - debugLog(`[PASSIVE RX] Ignoring: channel hash mismatch (packet=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')})`); - return; - } - - debugLog(`[PASSIVE RX] Channel hash match confirmed`); + // NOTE: Channel hash filter disabled to track ALL RX messages regardless of channel + // Uncomment to restore filtering to wardriving channel only + // const packetChannelHash = packet.payload[0]; + // if (WARDRIVING_CHANNEL_HASH !== null && packetChannelHash !== WARDRIVING_CHANNEL_HASH) { + // debugLog(`[PASSIVE RX] Ignoring: channel hash mismatch (packet=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')})`); + // return; + // } + // + // debugLog(`[PASSIVE RX] Channel hash match confirmed`); // VALIDATION STEP 3: Check path length (need at least one hop) if (packet.path.length === 0) { From 03a687e0d6fc8da993dae03ffbb7ca4f93c403cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:14:31 +0000 Subject: [PATCH 09/77] Update comment to accurately reflect validation steps after channel hash filter removal 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 1eb77f0..cab0382 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -1800,7 +1800,7 @@ async function handlePassiveRxLogEvent(data) { debugLog(`[PASSIVE RX] Header validation passed: 0x${packet.header.toString(16).padStart(2, '0')}`); - // VALIDATION STEP 2: Verify this message is for our wardriving channel + // VALIDATION STEP 2: Check payload length if (packet.payload.length < 3) { debugLog(`[PASSIVE RX] Ignoring: payload too short`); return; From 42bd79d94f741123dd8127a5cbe6d831a9160532 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:25:31 +0000 Subject: [PATCH 10/77] Initial plan From 82d13294835ebbd2f1104e0de46f811ce637b346 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:28:58 +0000 Subject: [PATCH 11/77] Add echo filtering to passive RX log listener Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 58 +++++++++++++++++++++++++++++++++++++-------- index.html | 2 +- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index cab0382..d552e32 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -1781,6 +1781,7 @@ function formatRepeaterTelemetry(repeaters) { /** * Handle passive RX log event - monitors all incoming packets * Extracts the LAST hop from the path (direct repeater) and records observation + * FILTERING: Excludes echoes of user's own pings on the wardriving channel * @param {Object} data - The LogRxData event data (contains lastSnr, lastRssi, raw) */ async function handlePassiveRxLogEvent(data) { @@ -1806,17 +1807,48 @@ async function handlePassiveRxLogEvent(data) { return; } - // NOTE: Channel hash filter disabled to track ALL RX messages regardless of channel - // Uncomment to restore filtering to wardriving channel only - // const packetChannelHash = packet.payload[0]; - // if (WARDRIVING_CHANNEL_HASH !== null && packetChannelHash !== WARDRIVING_CHANNEL_HASH) { - // debugLog(`[PASSIVE RX] Ignoring: channel hash mismatch (packet=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')})`); - // return; - // } - // - // debugLog(`[PASSIVE RX] Channel hash match confirmed`); + // VALIDATION STEP 3: Echo filtering for wardriving channel + // Check if this packet is on the wardriving channel + const packetChannelHash = packet.payload[0]; + const isWardrivingChannel = WARDRIVING_CHANNEL_HASH !== null && packetChannelHash === WARDRIVING_CHANNEL_HASH; + + if (isWardrivingChannel) { + debugLog(`[PASSIVE RX] Packet is on wardriving channel (hash=0x${packetChannelHash.toString(16).padStart(2, '0')})`); + + // Check if we have a recently-sent payload to compare against + if (state.repeaterTracking.sentPayload && WARDRIVING_CHANNEL_KEY) { + debugLog(`[PASSIVE RX] Checking if this is an echo of our own ping...`); + + // Decrypt the message to compare with our sent payload + const decryptedMessage = await decryptGroupTextPayload(packet.payload, WARDRIVING_CHANNEL_KEY); + + if (decryptedMessage !== null) { + debugLog(`[PASSIVE RX] Decrypted message: "${decryptedMessage}"`); + debugLog(`[PASSIVE RX] Our sent payload: "${state.repeaterTracking.sentPayload}"`); + + // Check if this is an echo of our own ping + // The decrypted message may include sender prefix: "SenderName: Message" + // So check for both exact match and containment + const isOurEcho = decryptedMessage === state.repeaterTracking.sentPayload || + decryptedMessage.includes(state.repeaterTracking.sentPayload); + + if (isOurEcho) { + debugLog(`[PASSIVE RX] ⊘ SKIP: This is an echo of our own ping - already tracked in Session Log`); + return; + } + + debugLog(`[PASSIVE RX] This is NOT an echo (different message on same channel)`); + } else { + debugLog(`[PASSIVE RX] Failed to decrypt message, cannot verify if echo`); + } + } else { + debugLog(`[PASSIVE RX] No sent payload to compare (not currently tracking echoes)`); + } + } else { + debugLog(`[PASSIVE RX] Packet is on a different channel or channel hash unavailable - logging it`); + } - // VALIDATION STEP 3: Check path length (need at least one hop) + // VALIDATION STEP 4: Check path length (need at least one hop) if (packet.path.length === 0) { debugLog(`[PASSIVE RX] Ignoring: no path (direct transmission, not via repeater)`); return; @@ -2999,6 +3031,12 @@ async function connect() { // Clean up all timers cleanupAllTimers(); + // Clear RX log entries on disconnect + rxLogState.entries = []; + renderRxLogEntries(); + updateRxLogSummary(); + debugLog("RX log cleared on disconnect"); + state.lastFix = null; state.lastSuccessfulPingLocation = null; state.gpsState = "idle"; diff --git a/index.html b/index.html index e31f6e6..b54635f 100644 --- a/index.html +++ b/index.html @@ -66,7 +66,7 @@

+
From b185e0c599757da3b5a73cb7621f72dbf31e004c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:10:54 +0000 Subject: [PATCH 52/77] fix: Remove 'Queued (X/50)' from dynamic status bar, restore debug log Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 9cdbb99..a47bc76 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -329,7 +329,7 @@ function applyStatusImmediately(text, color) { statusMessageState.lastSetTime = Date.now(); statusMessageState.currentText = text; statusMessageState.currentColor = color; - // Debug log removed - status updates are already logged earlier in the flow + debugLog(`Status applied: "${text}"`); } /** @@ -1532,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 } /** From 1e439d113bca2b233784602dc9ae80fe04c68571 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:14:58 +0000 Subject: [PATCH 53/77] feat: Add debug log tags (partial - INIT, UI, TIMER, WAKE LOCK, GEOFENCE) Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 84 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index a47bc76..1c3ba91 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -75,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 } })(); @@ -278,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; } @@ -297,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 }; @@ -329,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}"`); } /** @@ -460,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; } } @@ -476,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; } } @@ -501,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(); } @@ -549,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; } @@ -561,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); @@ -616,7 +616,7 @@ function cleanupAllTimers() { } } state.rxBatchBuffer.clear(); - debugLog("RX batch buffer cleared"); + debugLog("[RX BATCH] RX batch buffer cleared"); } } @@ -653,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); } @@ -699,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}`; @@ -744,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; } } @@ -801,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; @@ -817,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; } @@ -828,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; } @@ -845,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; } From 0c41695ef69f771c7307cddf0d0ae43a4aaedc73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:20:51 +0000 Subject: [PATCH 54/77] feat: Complete debug log tagging and update documentation Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 430 +++++++++++++++---------------- docs/DEVELOPMENT_REQUIREMENTS.md | 42 +++ 2 files changed, 257 insertions(+), 215 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 1c3ba91..7d242a6 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -731,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; } @@ -956,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, @@ -983,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); @@ -999,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(); @@ -1018,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, @@ -1031,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); @@ -1098,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; } @@ -1107,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.` ); @@ -1135,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 { @@ -1149,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}` @@ -1173,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; @@ -1230,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 } @@ -1247,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", @@ -1256,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; } @@ -1267,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") { @@ -1278,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; } } @@ -1299,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; } @@ -1322,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 } @@ -1346,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", @@ -1354,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}`); } } @@ -1403,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); } @@ -1443,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 = { @@ -1461,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 @@ -1484,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() } } @@ -1640,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; } @@ -1666,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) { @@ -1844,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; @@ -1865,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. @@ -1965,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 @@ -1983,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 @@ -2018,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 @@ -2032,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; @@ -2563,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'; @@ -2593,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; } @@ -2622,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; } @@ -2632,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`; @@ -2649,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) { @@ -2658,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; } @@ -2668,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`); } /** @@ -3129,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, @@ -3151,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; } @@ -3162,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, @@ -3192,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, @@ -3205,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, @@ -3215,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"); } @@ -3288,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}`); } /** @@ -3315,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`); } /** @@ -3323,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; } @@ -3336,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) { @@ -3366,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"; @@ -3385,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"; @@ -3407,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 @@ -3456,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); } @@ -3465,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 @@ -3489,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"); } } @@ -3507,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 @@ -3528,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 @@ -3538,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; } @@ -3566,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); @@ -3585,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; } @@ -3602,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; } @@ -3619,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; } @@ -3649,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(); @@ -3714,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"); } @@ -3809,27 +3809,27 @@ async function connect() { rxLogState.entries = []; 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; } @@ -3846,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(); @@ -3854,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 } } @@ -3865,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 } @@ -3878,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 { @@ -3902,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. } }); @@ -3930,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"); } } @@ -3941,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(); @@ -3957,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); @@ -3983,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"); @@ -4002,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"); @@ -4014,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(); }); }); @@ -4022,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(); }); } @@ -4044,12 +4044,12 @@ export async function onLoad() { } // 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 From 59abaaaa5eb49f76bf45693a5cab1be3528a5c0e Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Mon, 22 Dec 2025 21:37:24 -0500 Subject: [PATCH 55/77] fix: Correct string interpolation syntax in debugError logging --- content/wardrive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/wardrive.js b/content/wardrive.js index 7d242a6..8826f59 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -3957,7 +3957,7 @@ export async function onLoad() { await connect(); } } catch (e) { - debugError("[UI] Connection button error:" ${e.message}`, e); + debugError("[UI] Connection button error:" `${e.message}`, e); setDynamicStatus(e.message || "Connection failed", STATUS_COLORS.error); } }); From 017db55be04b1c8370bf589642de47e06973ad93 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Mon, 22 Dec 2025 22:04:23 -0500 Subject: [PATCH 56/77] feat: Add application version to payload in checkCapacity function --- content/wardrive.js | 1 + 1 file changed, 1 insertion(+) diff --git a/content/wardrive.js b/content/wardrive.js index 8826f59..eec3558 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -1243,6 +1243,7 @@ async function checkCapacity(reason) { const payload = { key: MESHMAPPER_API_KEY, public_key: state.devicePublicKey, + ver: APP_VERSION, who: getDeviceIdentifier(), reason: reason }; From afbc57bde3686527d84a80b5b898cdbfe8313700 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:37:27 +0000 Subject: [PATCH 57/77] Initial plan From b35b4a599cdb3ba4316180a73315c44890d10152 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:45:21 +0000 Subject: [PATCH 58/77] Add SNR chip and CSV copy buttons to all log sections Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/style.css | 67 +++++++++++++ content/wardrive.js | 229 ++++++++++++++++++++++++++++++++++++++++++-- index.html | 10 ++ 3 files changed, 299 insertions(+), 7 deletions(-) diff --git a/content/style.css b/content/style.css index 2b7ae46..882c4be 100644 --- a/content/style.css +++ b/content/style.css @@ -298,4 +298,71 @@ body, .chipSnr { font-size: 0.625rem; } +} + +/* Mini SNR Chip for Summary Bar */ +.chip-mini { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.5rem; + border-radius: 999px; + font-size: 0.625rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-weight: 600; + background: rgba(51, 65, 85, 0.6); + border: 1px solid rgba(71, 85, 105, 0.8); + white-space: nowrap; +} + +/* Mini chip SNR color coding */ +.chip-mini.snr-red { + color: #f87171; + border-color: rgba(248, 113, 113, 0.3); +} + +.chip-mini.snr-orange { + color: #fb923c; + border-color: rgba(251, 146, 60, 0.3); +} + +.chip-mini.snr-green { + color: #4ade80; + border-color: rgba(74, 222, 128, 0.3); +} + +/* Copy Button Styling */ +.copy-btn { + cursor: pointer; + border: 1px solid rgba(71, 85, 105, 0.5); + background: rgba(51, 65, 85, 0.3); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + user-select: none; +} + +.copy-btn:hover { + background: rgba(71, 85, 105, 0.5); + border-color: rgba(100, 116, 139, 0.7); +} + +.copy-btn:active { + transform: scale(0.95); +} + +.copy-btn.copied { + background: rgba(74, 222, 128, 0.2); + border-color: rgba(74, 222, 128, 0.5); + color: #4ade80; +} + +/* Mobile adjustments */ +@media (max-width: 640px) { + .chip-mini { + font-size: 0.55rem; + padding: 0.15rem 0.4rem; + } + + .copy-btn { + font-size: 0.65rem; + padding: 0.25rem 0.5rem; + } } \ No newline at end of file diff --git a/content/wardrive.js b/content/wardrive.js index eec3558..23455cd 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -156,6 +156,8 @@ const logScrollContainer = $("logScrollContainer"); const logCount = $("logCount"); const logLastTime = $("logLastTime"); const logLastSnr = $("logLastSnr"); +const logLastSnrChip = $("logLastSnrChip"); +const sessionLogCopyBtn = $("sessionLogCopyBtn"); // RX Log selectors const rxLogSummaryBar = $("rxLogSummaryBar"); @@ -166,6 +168,7 @@ const rxLogLastTime = $("rxLogLastTime"); const rxLogLastRepeater = $("rxLogLastRepeater"); const rxLogEntries = $("rxLogEntries"); const rxLogExpandArrow = $("rxLogExpandArrow"); +const rxLogCopyBtn = $("rxLogCopyBtn"); // Error Log selectors const errorLogSummaryBar = $("errorLogSummaryBar"); @@ -176,6 +179,7 @@ const errorLogLastTime = $("errorLogLastTime"); const errorLogLastError = $("errorLogLastError"); const errorLogEntries = $("errorLogEntries"); const errorLogExpandArrow = $("errorLogExpandArrow"); +const errorLogCopyBtn = $("errorLogCopyBtn"); // Session log state const sessionLogState = { @@ -2136,8 +2140,8 @@ async function handlePassiveRxLogging(packet, data) { const lon = state.lastFix.lon; const timestamp = new Date().toISOString(); - // Add entry to RX log - addRxLogEntry(repeaterId, data.lastSnr, lat, lon, timestamp); + // Add entry to RX log (including RSSI, path length, and header for CSV export) + addRxLogEntry(repeaterId, data.lastSnr, data.lastRssi, packet.path.length, packet.header, lat, lon, timestamp); debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${data.lastSnr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); @@ -2225,8 +2229,8 @@ async function handlePassiveRxLogEvent(data) { const lon = state.lastFix.lon; const timestamp = new Date().toISOString(); - // Add entry to RX log - addRxLogEntry(repeaterId, data.lastSnr, lat, lon, timestamp); + // Add entry to RX log (including RSSI, path length, and header for CSV export) + addRxLogEntry(repeaterId, data.lastSnr, data.lastRssi, packet.path.length, packet.header, lat, lon, timestamp); debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${data.lastSnr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); @@ -2623,7 +2627,11 @@ function updateLogSummary() { if (count === 0) { logLastTime.textContent = 'No data'; logLastSnr.textContent = '—'; - debugLog('[UI] Session log summary updated: no entries'); + // Hide SNR chip when no entries + if (logLastSnrChip) { + logLastSnrChip.classList.add('hidden'); + } + debugLog('[SESSION LOG] Session log summary updated: no entries'); return; } @@ -2633,14 +2641,35 @@ function updateLogSummary() { // Count total heard repeats in the latest ping const heardCount = lastEntry.events.length; - debugLog(`[UI] Session log summary updated: ${count} total pings, latest ping heard ${heardCount} repeats`); + debugLog(`[SESSION LOG] Session log summary updated: ${count} total pings, latest ping heard ${heardCount} repeats`); if (heardCount > 0) { logLastSnr.textContent = heardCount === 1 ? '1 Repeat' : `${heardCount} Repeats`; logLastSnr.className = 'text-xs font-mono text-slate-300'; + + // Find best (highest) SNR from events + let bestSnr = -Infinity; + lastEntry.events.forEach(event => { + if (event.value > bestSnr) { + bestSnr = event.value; + } + }); + + // Update SNR chip + if (logLastSnrChip && bestSnr !== -Infinity) { + const snrClass = getSnrSeverityClass(bestSnr); + logLastSnrChip.className = `chip-mini ${snrClass}`; + logLastSnrChip.textContent = `${bestSnr.toFixed(2)} dB`; + logLastSnrChip.classList.remove('hidden'); + debugLog(`[SESSION LOG] Best SNR chip updated: ${bestSnr.toFixed(2)} dB (${snrClass})`); + } } else { logLastSnr.textContent = '0 Repeats'; logLastSnr.className = 'text-xs font-mono text-slate-500'; + // Hide SNR chip when no repeats + if (logLastSnrChip) { + logLastSnrChip.classList.add('hidden'); + } } } @@ -2737,6 +2766,9 @@ function parseRxLogEntry(entry) { return { repeaterId: entry.repeaterId, snr: entry.snr, + rssi: entry.rssi, + pathLength: entry.pathLength, + header: entry.header, lat: entry.lat.toFixed(5), lon: entry.lon.toFixed(5), timestamp: entry.timestamp @@ -2902,10 +2934,13 @@ function toggleRxLogBottomSheet() { * @param {number} lon - Longitude * @param {string} timestamp - ISO timestamp */ -function addRxLogEntry(repeaterId, snr, lat, lon, timestamp) { +function addRxLogEntry(repeaterId, snr, rssi, pathLength, header, lat, lon, timestamp) { const entry = { repeaterId, snr, + rssi, + pathLength, + header, lat, lon, timestamp @@ -3117,6 +3152,161 @@ function addErrorLogEntry(message, source = null) { debugLog(`[ERROR LOG] Added entry: ${message.substring(0, 50)}${message.length > 50 ? '...' : ''}`); } +// ---- CSV Export Functions ---- + +/** + * Convert Session Log to CSV format + * Columns: Timestamp,Latitude,Longitude,Repeater1_ID,Repeater1_SNR,Repeater2_ID,Repeater2_SNR,... + * @returns {string} CSV formatted string + */ +function sessionLogToCSV() { + debugLog('[SESSION LOG] Converting session log to CSV format'); + + if (sessionLogState.entries.length === 0) { + debugWarn('[SESSION LOG] No session log entries to export'); + return 'Timestamp,Latitude,Longitude\n'; + } + + // Build CSV header - dynamic based on max number of repeaters + let maxRepeaters = 0; + sessionLogState.entries.forEach(entry => { + if (entry.events.length > maxRepeaters) { + maxRepeaters = entry.events.length; + } + }); + + let header = 'Timestamp,Latitude,Longitude'; + for (let i = 1; i <= maxRepeaters; i++) { + header += `,Repeater${i}_ID,Repeater${i}_SNR`; + } + header += '\n'; + + // Build CSV rows + const rows = sessionLogState.entries.map(entry => { + let row = `${entry.timestamp},${entry.lat},${entry.lon}`; + + // Add repeater data + entry.events.forEach(event => { + row += `,${event.type},${event.value.toFixed(2)}`; + }); + + return row; + }); + + const csv = header + rows.join('\n'); + debugLog(`[SESSION LOG] CSV export complete: ${sessionLogState.entries.length} entries`); + return csv; +} + +/** + * Convert RX Log to CSV format + * Columns: Timestamp,SNR,RSSI,RepeaterID,PathLength,Header + * @returns {string} CSV formatted string + */ +function rxLogToCSV() { + debugLog('[PASSIVE RX UI] Converting RX log to CSV format'); + + if (rxLogState.entries.length === 0) { + debugWarn('[PASSIVE RX UI] No RX log entries to export'); + return 'Timestamp,SNR,RSSI,RepeaterID,PathLength,Header\n'; + } + + const header = 'Timestamp,SNR,RSSI,RepeaterID,PathLength,Header\n'; + + const rows = rxLogState.entries.map(entry => { + const headerHex = '0x' + entry.header.toString(16).padStart(2, '0'); + return `${entry.timestamp},${entry.snr.toFixed(2)},${entry.rssi},${entry.repeaterId},${entry.pathLength},${headerHex}`; + }); + + const csv = header + rows.join('\n'); + debugLog(`[PASSIVE RX UI] CSV export complete: ${rxLogState.entries.length} entries`); + return csv; +} + +/** + * Convert Error Log to CSV format + * Columns: Timestamp,ErrorType,Message + * @returns {string} CSV formatted string + */ +function errorLogToCSV() { + debugLog('[ERROR LOG] Converting error log to CSV format'); + + if (errorLogState.entries.length === 0) { + debugWarn('[ERROR LOG] No error log entries to export'); + return 'Timestamp,ErrorType,Message\n'; + } + + const header = 'Timestamp,ErrorType,Message\n'; + + const rows = errorLogState.entries.map(entry => { + const source = entry.source || 'ERROR'; + // Escape quotes in message + const message = entry.message.replace(/"/g, '""'); + return `${entry.timestamp},${source},"${message}"`; + }); + + const csv = header + rows.join('\n'); + debugLog(`[ERROR LOG] CSV export complete: ${errorLogState.entries.length} entries`); + return csv; +} + +/** + * Copy log data to clipboard as CSV + * @param {string} logType - Type of log: 'session', 'rx', or 'error' + * @param {HTMLButtonElement} button - The button element that triggered the copy + */ +async function copyLogToCSV(logType, button) { + try { + debugLog(`[UI] Copy to CSV requested for ${logType} log`); + + let csv; + let logTag; + + switch (logType) { + case 'session': + csv = sessionLogToCSV(); + logTag = '[SESSION LOG]'; + break; + case 'rx': + csv = rxLogToCSV(); + logTag = '[PASSIVE RX UI]'; + break; + case 'error': + csv = errorLogToCSV(); + logTag = '[ERROR LOG]'; + break; + default: + debugError('[UI] Unknown log type for CSV export:', logType); + return; + } + + // Copy to clipboard + await navigator.clipboard.writeText(csv); + debugLog(`${logTag} CSV data copied to clipboard`); + + // Show feedback + const originalText = button.textContent; + button.textContent = 'Copied!'; + button.classList.add('copied'); + + // Reset after 1.5 seconds + setTimeout(() => { + button.textContent = originalText; + button.classList.remove('copied'); + debugLog(`${logTag} Copy button feedback reset`); + }, 1500); + + } catch (error) { + debugError(`[UI] Failed to copy ${logType} log to clipboard:`, error.message); + // Show error feedback + const originalText = button.textContent; + button.textContent = 'Failed'; + setTimeout(() => { + button.textContent = originalText; + }, 1500); + } +} + // ---- Ping ---- /** * Acquire fresh GPS coordinates and update state @@ -4044,6 +4234,31 @@ export async function onLoad() { }); } + // Copy button event listeners + if (sessionLogCopyBtn) { + sessionLogCopyBtn.addEventListener("click", (e) => { + e.stopPropagation(); // Prevent triggering the summary bar toggle + debugLog("[SESSION LOG] Copy button clicked"); + copyLogToCSV('session', sessionLogCopyBtn); + }); + } + + if (rxLogCopyBtn) { + rxLogCopyBtn.addEventListener("click", (e) => { + e.stopPropagation(); // Prevent triggering the summary bar toggle + debugLog("[PASSIVE RX UI] Copy button clicked"); + copyLogToCSV('rx', rxLogCopyBtn); + }); + } + + if (errorLogCopyBtn) { + errorLogCopyBtn.addEventListener("click", (e) => { + e.stopPropagation(); // Prevent triggering the summary bar toggle + debugLog("[ERROR LOG] Copy button clicked"); + copyLogToCSV('error', errorLogCopyBtn); + }); + } + // Prompt location permission early (optional) debugLog("[GPS] Requesting initial location permission"); try { diff --git a/index.html b/index.html index ac08c89..e6c4a66 100644 --- a/index.html +++ b/index.html @@ -208,6 +208,10 @@

+ + @@ -237,6 +241,9 @@

+ @@ -263,6 +270,9 @@

+ From 2a0e0d28967248da1025066392c06bc675befc69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:51:04 +0000 Subject: [PATCH 59/77] Address code review feedback - update JSDoc and improve variable naming Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 23455cd..7bad456 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -2930,6 +2930,9 @@ function toggleRxLogBottomSheet() { * Add entry to RX log * @param {string} repeaterId - Repeater ID (hex) * @param {number} snr - Signal-to-noise ratio + * @param {number} rssi - Received Signal Strength Indicator + * @param {number} pathLength - Number of hops in packet path + * @param {number} header - Packet header byte * @param {number} lat - Latitude * @param {number} lon - Longitude * @param {string} timestamp - ISO timestamp @@ -3214,8 +3217,8 @@ function rxLogToCSV() { const header = 'Timestamp,SNR,RSSI,RepeaterID,PathLength,Header\n'; const rows = rxLogState.entries.map(entry => { - const headerHex = '0x' + entry.header.toString(16).padStart(2, '0'); - return `${entry.timestamp},${entry.snr.toFixed(2)},${entry.rssi},${entry.repeaterId},${entry.pathLength},${headerHex}`; + const formattedHeader = '0x' + entry.header.toString(16).padStart(2, '0'); + return `${entry.timestamp},${entry.snr.toFixed(2)},${entry.rssi},${entry.repeaterId},${entry.pathLength},${formattedHeader}`; }); const csv = header + rows.join('\n'); From 337fea3b1d0fe4edac0f05dd0acfc0ca904d6082 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:53:31 +0000 Subject: [PATCH 60/77] Fix CSV export edge cases and improve robustness Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 7bad456..10b590c 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -2647,13 +2647,8 @@ function updateLogSummary() { logLastSnr.textContent = heardCount === 1 ? '1 Repeat' : `${heardCount} Repeats`; logLastSnr.className = 'text-xs font-mono text-slate-300'; - // Find best (highest) SNR from events - let bestSnr = -Infinity; - lastEntry.events.forEach(event => { - if (event.value > bestSnr) { - bestSnr = event.value; - } - }); + // Find best (highest) SNR from events using Math.max + const bestSnr = Math.max(...lastEntry.events.map(event => event.value)); // Update SNR chip if (logLastSnrChip && bestSnr !== -Infinity) { @@ -3193,6 +3188,12 @@ function sessionLogToCSV() { row += `,${event.type},${event.value.toFixed(2)}`; }); + // Pad with empty cells if this entry has fewer repeaters than max + const missingColumns = (maxRepeaters - entry.events.length) * 2; // 2 columns per repeater (ID, SNR) + for (let i = 0; i < missingColumns; i++) { + row += ','; + } + return row; }); @@ -3217,8 +3218,12 @@ function rxLogToCSV() { const header = 'Timestamp,SNR,RSSI,RepeaterID,PathLength,Header\n'; const rows = rxLogState.entries.map(entry => { - const formattedHeader = '0x' + entry.header.toString(16).padStart(2, '0'); - return `${entry.timestamp},${entry.snr.toFixed(2)},${entry.rssi},${entry.repeaterId},${entry.pathLength},${formattedHeader}`; + // Handle potentially missing fields from old entries + const snr = entry.snr !== undefined ? entry.snr.toFixed(2) : ''; + const rssi = entry.rssi !== undefined ? entry.rssi : ''; + const pathLength = entry.pathLength !== undefined ? entry.pathLength : ''; + const formattedHeader = entry.header !== undefined ? '0x' + entry.header.toString(16).padStart(2, '0') : ''; + return `${entry.timestamp},${snr},${rssi},${entry.repeaterId},${pathLength},${formattedHeader}`; }); const csv = header + rows.join('\n'); @@ -3242,10 +3247,10 @@ function errorLogToCSV() { const header = 'Timestamp,ErrorType,Message\n'; const rows = errorLogState.entries.map(entry => { - const source = entry.source || 'ERROR'; - // Escape quotes in message + // Escape quotes in both source and message fields + const source = (entry.source || 'ERROR').replace(/"/g, '""'); const message = entry.message.replace(/"/g, '""'); - return `${entry.timestamp},${source},"${message}"`; + return `${entry.timestamp},"${source}","${message}"`; }); const csv = header + rows.join('\n'); From d30369966fa0f26f07fb6b1286d565aec468dfed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:56:49 +0000 Subject: [PATCH 61/77] Implement new requirement: Show copy buttons only when logs are expanded Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 50 +++++++++++++++++++++++++++++++++++++++++++++ index.html | 6 +++--- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 10b590c..5fd1554 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -2730,6 +2730,30 @@ function toggleBottomSheet() { logExpandArrow.classList.remove('expanded'); } } + + // Toggle copy button and status visibility + if (sessionLogState.isExpanded) { + // Hide status elements, show copy button + if (logLastSnr) logLastSnr.classList.add('hidden'); + if (logLastSnrChip) logLastSnrChip.classList.add('hidden'); + if (sessionLogCopyBtn) sessionLogCopyBtn.classList.remove('hidden'); + debugLog('[SESSION LOG] Expanded - showing copy button, hiding status'); + } else { + // Show status elements, hide copy button + if (logLastSnr) logLastSnr.classList.remove('hidden'); + if (logLastSnrChip) { + // Only show SNR chip if there are repeats (it manages its own visibility) + const count = sessionLogState.entries.length; + if (count > 0) { + const lastEntry = sessionLogState.entries[count - 1]; + if (lastEntry.events.length > 0) { + logLastSnrChip.classList.remove('hidden'); + } + } + } + if (sessionLogCopyBtn) sessionLogCopyBtn.classList.add('hidden'); + debugLog('[SESSION LOG] Collapsed - hiding copy button, showing status'); + } } /** @@ -2919,6 +2943,19 @@ function toggleRxLogBottomSheet() { rxLogExpandArrow.classList.remove('expanded'); } } + + // Toggle copy button and status visibility + if (rxLogState.isExpanded) { + // Hide status, show copy button + if (rxLogLastRepeater) rxLogLastRepeater.classList.add('hidden'); + if (rxLogCopyBtn) rxLogCopyBtn.classList.remove('hidden'); + debugLog('[PASSIVE RX UI] Expanded - showing copy button, hiding status'); + } else { + // Show status, hide copy button + if (rxLogLastRepeater) rxLogLastRepeater.classList.remove('hidden'); + if (rxLogCopyBtn) rxLogCopyBtn.classList.add('hidden'); + debugLog('[PASSIVE RX UI] Collapsed - hiding copy button, showing status'); + } } /** @@ -3118,6 +3155,19 @@ function toggleErrorLogBottomSheet() { errorLogExpandArrow.classList.remove('expanded'); } } + + // Toggle copy button and status visibility + if (errorLogState.isExpanded) { + // Hide status, show copy button + if (errorLogLastError) errorLogLastError.classList.add('hidden'); + if (errorLogCopyBtn) errorLogCopyBtn.classList.remove('hidden'); + debugLog('[ERROR LOG] Expanded - showing copy button, hiding status'); + } else { + // Show status, hide copy button + if (errorLogLastError) errorLogLastError.classList.remove('hidden'); + if (errorLogCopyBtn) errorLogCopyBtn.classList.add('hidden'); + debugLog('[ERROR LOG] Collapsed - hiding copy button, showing status'); + } } /** diff --git a/index.html b/index.html index e6c4a66..0d65ace 100644 --- a/index.html +++ b/index.html @@ -209,7 +209,7 @@

- @@ -241,7 +241,7 @@

- @@ -270,7 +270,7 @@

- From 43380a777fbd2ea094e63e20dc0919d794b4a85c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:15:45 +0000 Subject: [PATCH 62/77] Initial plan From 87f71f9e233e65d9a7ae784846845cb76f209488 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:20:17 +0000 Subject: [PATCH 63/77] Move SNR chip from Session Log to RX Log and update CSV export formats Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 94 +++++++++++++++++---------------------------- index.html | 2 +- 2 files changed, 36 insertions(+), 60 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 5fd1554..8848631 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -156,7 +156,6 @@ const logScrollContainer = $("logScrollContainer"); const logCount = $("logCount"); const logLastTime = $("logLastTime"); const logLastSnr = $("logLastSnr"); -const logLastSnrChip = $("logLastSnrChip"); const sessionLogCopyBtn = $("sessionLogCopyBtn"); // RX Log selectors @@ -166,6 +165,7 @@ const rxLogScrollContainer = $("rxLogScrollContainer"); const rxLogCount = $("rxLogCount"); const rxLogLastTime = $("rxLogLastTime"); const rxLogLastRepeater = $("rxLogLastRepeater"); +const rxLogSnrChip = $("rxLogSnrChip"); const rxLogEntries = $("rxLogEntries"); const rxLogExpandArrow = $("rxLogExpandArrow"); const rxLogCopyBtn = $("rxLogCopyBtn"); @@ -2627,10 +2627,6 @@ function updateLogSummary() { if (count === 0) { logLastTime.textContent = 'No data'; logLastSnr.textContent = '—'; - // Hide SNR chip when no entries - if (logLastSnrChip) { - logLastSnrChip.classList.add('hidden'); - } debugLog('[SESSION LOG] Session log summary updated: no entries'); return; } @@ -2646,25 +2642,9 @@ function updateLogSummary() { if (heardCount > 0) { logLastSnr.textContent = heardCount === 1 ? '1 Repeat' : `${heardCount} Repeats`; logLastSnr.className = 'text-xs font-mono text-slate-300'; - - // Find best (highest) SNR from events using Math.max - const bestSnr = Math.max(...lastEntry.events.map(event => event.value)); - - // Update SNR chip - if (logLastSnrChip && bestSnr !== -Infinity) { - const snrClass = getSnrSeverityClass(bestSnr); - logLastSnrChip.className = `chip-mini ${snrClass}`; - logLastSnrChip.textContent = `${bestSnr.toFixed(2)} dB`; - logLastSnrChip.classList.remove('hidden'); - debugLog(`[SESSION LOG] Best SNR chip updated: ${bestSnr.toFixed(2)} dB (${snrClass})`); - } } else { logLastSnr.textContent = '0 Repeats'; logLastSnr.className = 'text-xs font-mono text-slate-500'; - // Hide SNR chip when no repeats - if (logLastSnrChip) { - logLastSnrChip.classList.add('hidden'); - } } } @@ -2735,22 +2715,11 @@ function toggleBottomSheet() { if (sessionLogState.isExpanded) { // Hide status elements, show copy button if (logLastSnr) logLastSnr.classList.add('hidden'); - if (logLastSnrChip) logLastSnrChip.classList.add('hidden'); if (sessionLogCopyBtn) sessionLogCopyBtn.classList.remove('hidden'); debugLog('[SESSION LOG] Expanded - showing copy button, hiding status'); } else { // Show status elements, hide copy button if (logLastSnr) logLastSnr.classList.remove('hidden'); - if (logLastSnrChip) { - // Only show SNR chip if there are repeats (it manages its own visibility) - const count = sessionLogState.entries.length; - if (count > 0) { - const lastEntry = sessionLogState.entries[count - 1]; - if (lastEntry.events.length > 0) { - logLastSnrChip.classList.remove('hidden'); - } - } - } if (sessionLogCopyBtn) sessionLogCopyBtn.classList.add('hidden'); debugLog('[SESSION LOG] Collapsed - hiding copy button, showing status'); } @@ -2847,6 +2816,10 @@ function updateRxLogSummary() { if (count === 0) { rxLogLastTime.textContent = 'No data'; rxLogLastRepeater.textContent = '—'; + // Hide SNR chip when no entries + if (rxLogSnrChip) { + rxLogSnrChip.classList.add('hidden'); + } debugLog('[PASSIVE RX UI] Summary updated: no entries'); return; } @@ -2856,6 +2829,17 @@ function updateRxLogSummary() { rxLogLastTime.textContent = date.toLocaleTimeString(); rxLogLastRepeater.textContent = lastEntry.repeaterId; + // Update SNR chip + if (rxLogSnrChip && rxLogState.entries.length > 0) { + const snrClass = getSnrSeverityClass(lastEntry.snr); + rxLogSnrChip.className = `chip-mini ${snrClass}`; + rxLogSnrChip.textContent = `${lastEntry.snr.toFixed(2)} dB`; + rxLogSnrChip.classList.remove('hidden'); + debugLog(`[PASSIVE RX UI] SNR chip updated: ${lastEntry.snr.toFixed(2)} dB (${snrClass})`); + } else if (rxLogSnrChip) { + rxLogSnrChip.classList.add('hidden'); + } + debugLog(`[PASSIVE RX UI] Summary updated: ${count} observations, last repeater: ${lastEntry.repeaterId}`); } @@ -2948,11 +2932,15 @@ function toggleRxLogBottomSheet() { if (rxLogState.isExpanded) { // Hide status, show copy button if (rxLogLastRepeater) rxLogLastRepeater.classList.add('hidden'); + if (rxLogSnrChip) rxLogSnrChip.classList.add('hidden'); if (rxLogCopyBtn) rxLogCopyBtn.classList.remove('hidden'); debugLog('[PASSIVE RX UI] Expanded - showing copy button, hiding status'); } else { // Show status, hide copy button if (rxLogLastRepeater) rxLogLastRepeater.classList.remove('hidden'); + if (rxLogSnrChip && rxLogState.entries.length > 0) { + rxLogSnrChip.classList.remove('hidden'); + } if (rxLogCopyBtn) rxLogCopyBtn.classList.add('hidden'); debugLog('[PASSIVE RX UI] Collapsed - hiding copy button, showing status'); } @@ -3212,35 +3200,24 @@ function sessionLogToCSV() { if (sessionLogState.entries.length === 0) { debugWarn('[SESSION LOG] No session log entries to export'); - return 'Timestamp,Latitude,Longitude\n'; + return 'Timestamp,Latitude,Longitude,Repeats\n'; } - // Build CSV header - dynamic based on max number of repeaters - let maxRepeaters = 0; - sessionLogState.entries.forEach(entry => { - if (entry.events.length > maxRepeaters) { - maxRepeaters = entry.events.length; - } - }); - - let header = 'Timestamp,Latitude,Longitude'; - for (let i = 1; i <= maxRepeaters; i++) { - header += `,Repeater${i}_ID,Repeater${i}_SNR`; - } - header += '\n'; + // Fixed 4-column header + const header = 'Timestamp,Latitude,Longitude,Repeats\n'; // Build CSV rows const rows = sessionLogState.entries.map(entry => { let row = `${entry.timestamp},${entry.lat},${entry.lon}`; - // Add repeater data - entry.events.forEach(event => { - row += `,${event.type},${event.value.toFixed(2)}`; - }); - - // Pad with empty cells if this entry has fewer repeaters than max - const missingColumns = (maxRepeaters - entry.events.length) * 2; // 2 columns per repeater (ID, SNR) - for (let i = 0; i < missingColumns; i++) { + // Combine all repeater data into single Repeats column + // Format: repeaterID(snr)|repeaterID(snr)|... + if (entry.events.length > 0) { + const repeats = entry.events.map(event => { + return `${event.type}(${event.value.toFixed(2)})`; + }).join('|'); + row += `,${repeats}`; + } else { row += ','; } @@ -3254,7 +3231,7 @@ function sessionLogToCSV() { /** * Convert RX Log to CSV format - * Columns: Timestamp,SNR,RSSI,RepeaterID,PathLength,Header + * Columns: Timestamp,RepeaterID,SNR,RSSI,PathLength * @returns {string} CSV formatted string */ function rxLogToCSV() { @@ -3262,18 +3239,17 @@ function rxLogToCSV() { if (rxLogState.entries.length === 0) { debugWarn('[PASSIVE RX UI] No RX log entries to export'); - return 'Timestamp,SNR,RSSI,RepeaterID,PathLength,Header\n'; + return 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n'; } - const header = 'Timestamp,SNR,RSSI,RepeaterID,PathLength,Header\n'; + const header = 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n'; const rows = rxLogState.entries.map(entry => { // Handle potentially missing fields from old entries const snr = entry.snr !== undefined ? entry.snr.toFixed(2) : ''; const rssi = entry.rssi !== undefined ? entry.rssi : ''; const pathLength = entry.pathLength !== undefined ? entry.pathLength : ''; - const formattedHeader = entry.header !== undefined ? '0x' + entry.header.toString(16).padStart(2, '0') : ''; - return `${entry.timestamp},${snr},${rssi},${entry.repeaterId},${pathLength},${formattedHeader}`; + return `${entry.timestamp},${entry.repeaterId},${snr},${rssi},${pathLength}`; }); const csv = header + rows.join('\n'); diff --git a/index.html b/index.html index 0d65ace..8613b32 100644 --- a/index.html +++ b/index.html @@ -208,7 +208,6 @@

- @@ -241,6 +240,7 @@

+ From 7392143b9b8edb2cc535908d45a28b4213ae8501 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:22:55 +0000 Subject: [PATCH 64/77] Initial plan From 9c365dfac443bdf451f5727b7e8bfdedadfa5c27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:27:59 +0000 Subject: [PATCH 65/77] Add reason code handling for capacity check API responses - Added REASON_MESSAGES constant mapping for extensible reason handling - Updated checkCapacity() to parse and store reason codes from API - Modified disconnect event handler to check REASON_MESSAGES first - Added fallback for unknown reason codes with generic message - Updated documentation in STATUS_MESSAGES.md and CONNECTION_WORKFLOW.md - Added debug logging for reason code processing Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 33 ++++++++++++++++++++++++++------- docs/CONNECTION_WORKFLOW.md | 24 ++++++++++++++++++------ docs/STATUS_MESSAGES.md | 16 ++++++++++++++++ 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 5fd1554..e947cfc 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -118,6 +118,13 @@ const WARDIVE_IATA_CODE = "YOW"; // For DEV builds: Contains "DEV-" format (e.g., "DEV-1734652800") const APP_VERSION = "UNKNOWN"; // Placeholder - replaced during build +// ---- Capacity Check Reason Messages ---- +// Maps API reason codes to user-facing error messages +const REASON_MESSAGES = { + outofdate: "App out of date, please update", + // Future reasons can be added here +}; + // ---- UI helpers ---- // Status colors for different states const STATUS_COLORS = { @@ -228,7 +235,7 @@ const state = { capturedPingCoords: null, // { lat, lon, accuracy } captured at ping time, used for API post after 7s delay devicePublicKey: null, // Hex string of device's public key (used for capacity check) wardriveSessionId: null, // Session ID from capacity check API (used for all MeshMapper API posts) - disconnectReason: null, // Tracks the reason for disconnection (e.g., "app_down", "capacity_full", "public_key_error", "channel_setup_error", "ble_disconnect_error", "session_id_error", "normal") + disconnectReason: null, // Tracks the reason for disconnection (e.g., "app_down", "capacity_full", "public_key_error", "channel_setup_error", "ble_disconnect_error", "session_id_error", "normal", or API reason codes like "outofdate") channelSetupErrorMessage: null, // Error message from channel setup failure bleDisconnectErrorMessage: null, // Error message from BLE disconnect failure repeaterTracking: { @@ -1272,11 +1279,17 @@ async function checkCapacity(reason) { } const data = await response.json(); - debugLog(`[CAPACITY] 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'}, reason=${data.reason || 'none'}`); // Handle capacity full vs. allowed cases separately if (data.allowed === false && reason === "connect") { - state.disconnectReason = "capacity_full"; // Track disconnect reason + // Check if a reason code is provided + if (data.reason) { + debugLog(`[CAPACITY] API returned reason code: ${data.reason}`); + state.disconnectReason = data.reason; // Store the reason code directly + } else { + state.disconnectReason = "capacity_full"; // Default to capacity_full + } return false; } @@ -3985,7 +3998,13 @@ async function connect() { setConnStatus("Disconnected", STATUS_COLORS.error); // Set dynamic status based on disconnect reason (WITHOUT "Disconnected:" prefix) - if (state.disconnectReason === "capacity_full") { + // First check if reason has a mapped message in REASON_MESSAGES (for API reason codes) + if (state.disconnectReason && REASON_MESSAGES[state.disconnectReason]) { + debugLog(`[BLE] Branch: known reason code (${state.disconnectReason})`); + const errorMsg = REASON_MESSAGES[state.disconnectReason]; + setDynamicStatus(errorMsg, STATUS_COLORS.error, true); + debugLog(`[BLE] Setting terminal status for reason: ${state.disconnectReason}`); + } else if (state.disconnectReason === "capacity_full") { debugLog("[BLE] Branch: capacity_full"); setDynamicStatus("WarDriving app has reached capacity", STATUS_COLORS.error, true); debugLog("[BLE] Setting terminal status for capacity full"); @@ -4022,9 +4041,9 @@ async function connect() { setDynamicStatus("Idle"); // Show em dash for normal disconnect } else { debugLog(`[BLE] Branch: else (unknown reason: ${state.disconnectReason})`); - // For unknown disconnect reasons, show em dash - debugLog(`[BLE] Showing em dash for unknown reason: ${state.disconnectReason}`); - setDynamicStatus("Idle"); + // For unknown disconnect reasons from API, show a generic message + debugLog(`[BLE] Showing generic error for unknown reason: ${state.disconnectReason}`); + setDynamicStatus(`Connection not allowed: ${state.disconnectReason}`, STATUS_COLORS.error, true); } setConnectButton(false); diff --git a/docs/CONNECTION_WORKFLOW.md b/docs/CONNECTION_WORKFLOW.md index 78232df..fe9a689 100644 --- a/docs/CONNECTION_WORKFLOW.md +++ b/docs/CONNECTION_WORKFLOW.md @@ -142,11 +142,20 @@ connectBtn.addEventListener("click", async () => { "reason": "connect" } ``` - - If `allowed: false`: - - Sets `state.disconnectReason = "capacity_full"` - - Triggers disconnect sequence after 1.5s delay - - **Connection Status**: `"Connecting"` → `"Disconnecting"` → `"Disconnected"` (red) - - **Dynamic Status**: `"Acquiring wardriving slot"` → `"WarDriving app has reached capacity"` (red, terminal) + - If `allowed: false` with reason code: + - API response may include `reason` field: `{"allowed": false, "reason": "outofdate"}` + - If reason code exists in `REASON_MESSAGES` mapping: + - Sets `state.disconnectReason = data.reason` (e.g., "outofdate") + - Triggers disconnect sequence after 1.5s delay + - **Connection Status**: `"Connecting"` → `"Disconnecting"` → `"Disconnected"` (red) + - **Dynamic Status**: `"Acquiring wardriving slot"` → `"[mapped message]"` (red, terminal) + - Example: "App out of date, please update" for reason="outofdate" + - If reason code not in mapping: + - Sets `state.disconnectReason = data.reason` + - Shows fallback message: "Connection not allowed: [reason]" + - If no reason code provided (backward compatibility): + - Sets `state.disconnectReason = "capacity_full"` + - **Dynamic Status**: `"WarDriving app has reached capacity"` (red, terminal) - If API error: - Sets `state.disconnectReason = "app_down"` - Triggers disconnect sequence after 1.5s delay (fail-closed) @@ -237,10 +246,11 @@ See `content/wardrive.js` for the main `disconnect()` function. 2. **Set Disconnect Reason** - "normal" - user-initiated - - "capacity_full" - MeshMapper full + - "capacity_full" - MeshMapper full (no reason code) - "app_down" - API unavailable - "error" - validation/setup failure - "slot_revoked" - slot revoked during active session + - API reason codes (e.g., "outofdate") - specific denial reasons from capacity check API 3. **Update Status** - **Connection Status**: `"Disconnecting"` (blue) - remains until cleanup completes @@ -277,6 +287,7 @@ See `content/wardrive.js` for the main `disconnect()` function. - Fires on BLE disconnect - **Connection Status**: `"Disconnected"` (red) - ALWAYS set regardless of reason - **Dynamic Status**: Set based on `state.disconnectReason` (WITHOUT "Disconnected:" prefix): + - API reason codes in `REASON_MESSAGES` (e.g., `outofdate` → `"App out of date, please update"`) (red) - `capacity_full` → `"WarDriving app has reached capacity"` (red) - `app_down` → `"WarDriving app is down"` (red) - `slot_revoked` → `"WarDriving slot has been revoked"` (red) @@ -284,6 +295,7 @@ See `content/wardrive.js` for the main `disconnect()` function. - `channel_setup_error` → Error message (red) - `ble_disconnect_error` → Error message (red) - `normal` / `null` / `undefined` → `"—"` (em dash) + - Unknown reason codes → `"Connection not allowed: [reason]"` (red) - Runs comprehensive cleanup: - Stops auto-ping mode - Clears auto-ping timer diff --git a/docs/STATUS_MESSAGES.md b/docs/STATUS_MESSAGES.md index 69c9c61..fde7ad0 100644 --- a/docs/STATUS_MESSAGES.md +++ b/docs/STATUS_MESSAGES.md @@ -161,6 +161,22 @@ These messages appear in the Dynamic App Status Bar. They NEVER include connecti - **Notes**: Implements fail-closed policy - connection/posting denied if session_id is missing. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "Session ID error; try reconnecting" (terminal) - **Source**: `content/wardrive.js:checkCapacity()`, `content/wardrive.js:postToMeshMapperAPI()` +##### App out of date, please update +- **Message**: `"App out of date, please update"` +- **Color**: Red (error) +- **When**: Capacity check API denies slot on connect with reason code "outofdate" (returns allowed=false, reason="outofdate") +- **Terminal State**: Yes (persists until user takes action) +- **Notes**: Indicates the app version is outdated and needs to be updated. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "App out of date, please update" (terminal). This is part of the extensible reason code system - future reason codes can be added to REASON_MESSAGES mapping. +- **Source**: `content/wardrive.js:checkCapacity()`, `content/wardrive.js` disconnected event handler + +##### Connection not allowed: [reason] +- **Message**: `"Connection not allowed: [reason]"` (where [reason] is the API-provided reason code) +- **Color**: Red (error) +- **When**: Capacity check API denies slot on connect with an unknown reason code not defined in REASON_MESSAGES mapping (returns allowed=false, reason="unknown_code") +- **Terminal State**: Yes (persists until user takes action) +- **Notes**: Fallback message for future/unknown reason codes. Shows the raw reason code to help with debugging. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "Connection not allowed: [reason]" (terminal) +- **Source**: `content/wardrive.js` disconnected event handler + ##### Error: No session ID for API post - **Message**: `"Error: No session ID for API post"` - **Color**: Red (error) From 35c8c8bd719562bd3ed1f7f10a06868024e994d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:38:36 +0000 Subject: [PATCH 66/77] Initial plan From bf1f756f67e901ed939657980f10d6a526690422 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:42:05 +0000 Subject: [PATCH 67/77] Remove index-new.html and deprecated functions from wardrive.js Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 95 +----------------- index-new.html | 237 -------------------------------------------- tailwind.config.js | 1 - 3 files changed, 2 insertions(+), 331 deletions(-) delete mode 100644 index-new.html diff --git a/content/wardrive.js b/content/wardrive.js index c88ce21..6f740ec 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -107,7 +107,7 @@ const MESHMAPPER_API_URL = "https://yow.meshmapper.net/wardriving-api.php"; const MESHMAPPER_CAPACITY_CHECK_URL = "https://yow.meshmapper.net/capacitycheck.php"; const MESHMAPPER_API_KEY = "59C7754DABDF5C11CA5F5D8368F89"; const MESHMAPPER_DEFAULT_WHO = "GOME-WarDriver"; // Default identifier -const MESHMAPPER_RX_LOG_API_URL = "https://yow.meshmapper.net/wardriving-api.php"; // TODO: Set when API endpoint is ready +const MESHMAPPER_RX_LOG_API_URL = "https://yow.meshmapper.net/wardriving-api.php"; // Static for now; will be made dynamic later. const WARDIVE_IATA_CODE = "YOW"; @@ -2166,91 +2166,7 @@ async function handlePassiveRxLogging(packet, data) { } } -// DEPRECATED: Old separate passive RX handler - keeping for reference during migration -/** - * Handle passive RX log event - monitors all incoming packets - * Extracts the LAST hop from the path (direct repeater) and records observation - * FILTERING: Excludes echoes of user's own pings on the wardriving channel - * @param {Object} data - The LogRxData event data (contains lastSnr, lastRssi, raw) - * @deprecated Use handleUnifiedRxLogEvent instead - */ -async function handlePassiveRxLogEvent(data) { - try { - debugLog(`[PASSIVE RX] Received rx_log entry: SNR=${data.lastSnr}, RSSI=${data.lastRssi}`); - - // Parse the packet from raw data - const packet = Packet.fromBytes(data.raw); - - // VALIDATION STEP 1: Header validation - // Expected header for channel GroupText packets: 0x15 - const EXPECTED_HEADER = 0x15; - if (packet.header !== EXPECTED_HEADER) { - debugLog(`[PASSIVE RX] Ignoring: header validation failed (header=0x${packet.header.toString(16).padStart(2, '0')})`); - return; - } - - debugLog(`[PASSIVE RX] Header validation passed: 0x${packet.header.toString(16).padStart(2, '0')}`); - - // VALIDATION STEP 2: Check payload length - if (packet.payload.length < 3) { - debugLog(`[PASSIVE RX] Ignoring: payload too short`); - return; - } - - // VALIDATION STEP 3: Echo filtering for wardriving channel - // OPTIMIZATION: Skip processing if Session Log handler is actively tracking - // This avoids double-decryption during the 7-second echo window - if (state.repeaterTracking.isListening) { - debugLog(`[PASSIVE RX] ⊘ SKIP: Session Log is actively tracking echoes (7-second window) - avoiding double processing`); - return; - } - - // Check if this packet is on the wardriving channel - const packetChannelHash = packet.payload[0]; - const isWardrivingChannel = WARDRIVING_CHANNEL_HASH !== null && packetChannelHash === WARDRIVING_CHANNEL_HASH; - - if (isWardrivingChannel) { - debugLog(`[PASSIVE RX] Packet is on wardriving channel (hash=0x${packetChannelHash.toString(16).padStart(2, '0')})`); - - // Note: We don't need to check sentPayload here anymore since we skip entirely - // during active tracking window. Any messages on wardriving channel after the - // tracking window ends are from other users and should be logged. - debugLog(`[PASSIVE RX] Not in tracking window - message is from another user or source`); - } else { - debugLog(`[PASSIVE RX] Packet is on a different channel or channel hash unavailable - logging it`); - } - - // VALIDATION STEP 4: Check path length (need at least one hop) - if (packet.path.length === 0) { - debugLog(`[PASSIVE RX] Ignoring: no path (direct transmission, not via repeater)`); - return; - } - - // Extract LAST hop from path (the repeater that directly delivered to us) - const lastHopId = packet.path[packet.path.length - 1]; - const repeaterId = lastHopId.toString(16).padStart(2, '0'); - - debugLog(`[PASSIVE RX] Packet heard via last hop: ${repeaterId}, SNR=${data.lastSnr}, path_length=${packet.path.length}`); - - // Get current GPS location - if (!state.lastFix) { - debugLog(`[PASSIVE RX] No GPS fix available, skipping entry`); - return; - } - - const lat = state.lastFix.lat; - const lon = state.lastFix.lon; - const timestamp = new Date().toISOString(); - - // Add entry to RX log (including RSSI, path length, and header for CSV export) - addRxLogEntry(repeaterId, data.lastSnr, data.lastRssi, packet.path.length, packet.header, lat, lon, timestamp); - - debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${data.lastSnr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); - - } catch (error) { - debugError(`[PASSIVE RX] Error processing rx_log entry: ${error.message}`, error); - } -} + /** * Start unified RX listening - handles both Session Log tracking and passive RX logging @@ -2297,14 +2213,7 @@ function stopUnifiedRxListening() { debugLog(`[UNIFIED RX] ✅ Unified listening stopped`); } -// DEPRECATED: Keeping aliases for backward compatibility during migration -function startPassiveRxListening() { - startUnifiedRxListening(); -} -function stopPassiveRxListening() { - stopUnifiedRxListening(); -} /** * Future: Post RX log data to MeshMapper API diff --git a/index-new.html b/index-new.html deleted file mode 100644 index fd369bc..0000000 --- a/index-new.html +++ /dev/null @@ -1,237 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2818 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;\f1\fnil\fcharset77 ZapfDingbatsITC;} -{\colortbl;\red255\green255\blue255;} -{\*\expandedcolortbl;;} -\margl1440\margr1440\vieww11520\viewh8400\viewkind0 -\deftab560 -\pard\pardeftab560\slleading20\partightenfactor0 - -\f0\fs26 \cf0 \ -\ -\ - \ - \ - \ - MeshCore Wardrive (Ottawa)\ - \ - \ - \ - \ - \ - \ - \ - \ -\ - \ - \ - \ -\ - \ - \ -\ - \ -\ -\ -\ -
\ -
\ -\ - \ -
\ -

\ - MeshCore\ - MeshCore Wardrive\ -

\ - v1.0\ -
\ -\ - \ -
\ -
\ - \uc0\u9679 \ - Disconnected\ -
\ - \ -
\ -\ - \ - \ -\ - \ - \ -\ - \ -
\ - \ - \ - \ - \ -
\ - \ -
\ - \'b1-\ -
\ - \ - \ -
\ -
-
\ -
-
\ -
\ - \ - \ -
\ - -\ -
\ -
\ -
\ -\ - \ -
\ -

\ - Select radio power to connect\ -

\ -\ - \ - \ -\ - \ - \ -
\ -\ - \ -
\ -
\ -

Session Pings

\ -
\ -
    \ - \ -
\ -
\ - \ - \ -
\ -

Notes

\ -
    \ -
  • Requires Bluetooth and Location permissions
  • \ -
  • Keep app in foreground with screen on & unlocked
  • \ -
  • YOW region only
  • \ -
  • Sends location to #wardriving for coverage map
  • \ -
  • \uc0\u9888 \u65039 Not supported in Safari \'97 Use Bluefy on iOS
  • \ -
\ -

\ - Fork of kallanreed/mesh-map,\ - modified for meshmapper.net\ -

\ -
\ -\ -
\ -
\ - \ - \ - \ - \ -\ -} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index d461b79..a6532ce 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -2,7 +2,6 @@ module.exports = { content: [ "./index.html", - "./index-new.html", "./content/**/*.js", "./content/**/*.html" ] From cef63197e3efe146acf0684ff8526d515df583e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:46:34 +0000 Subject: [PATCH 68/77] Fix syntax error and extract magic number constant Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- OPTIMIZATION_REPORT.md | 206 +++++++++++++++++++++++++++++++++++++++++ content/wardrive.js | 11 ++- 2 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 OPTIMIZATION_REPORT.md diff --git a/OPTIMIZATION_REPORT.md b/OPTIMIZATION_REPORT.md new file mode 100644 index 0000000..68b82d9 --- /dev/null +++ b/OPTIMIZATION_REPORT.md @@ -0,0 +1,206 @@ +# wardrive.js Optimization Report + +## Executive Summary +This report documents the analysis and optimization of `wardrive.js`, which has been reduced from 4,324 lines to 4,233 lines (-91 lines, -2.1%) in the initial cleanup phase. + +## Files Removed +- **index-new.html** (237 lines) - Unused HTML file deleted +- **tailwind.config.js** - Removed reference to index-new.html + +## Code Cleanup Completed + +### 1. Dead Code Removal (91 lines removed) +- ✅ Removed deprecated `handlePassiveRxLogEvent()` function (77 lines) + - Was marked as @deprecated and replaced by `handleUnifiedRxLogEvent()` + - No longer called anywhere in the codebase + +- ✅ Removed deprecated alias functions (7 lines) + - `startPassiveRxListening()` - alias for `startUnifiedRxListening()` + - `stopPassiveRxListening()` - alias for `stopUnifiedRxListening()` + +- ✅ Cleaned up TODO comments + - Removed "TODO: Set when API endpoint is ready" comment (endpoint is already set) + +## Code Analysis - Opportunities Identified + +### 1. Duplicate Code Patterns + +#### A. Bottom Sheet Toggle Functions (High Similarity ~90%) +Three nearly identical functions with only minor variations: +- `toggleBottomSheet()` - Lines 2613-2648 +- `toggleRxLogBottomSheet()` - Lines 2831-2869 +- `toggleErrorLogBottomSheet()` - Lines 3047-3081 + +**Pattern**: All follow same structure: +1. Toggle isExpanded state +2. Add/remove 'open' and 'hidden' classes +3. Rotate arrow element +4. Show/hide copy button and status elements +5. Log debug messages + +**Potential consolidation**: Create generic `toggleLogBottomSheet(config)` helper +**Risk**: Medium - UI-critical code, needs careful testing +**Benefit**: ~90 lines → ~40 lines (-50 lines) + +#### B. Log Entry Rendering Functions (High Similarity ~85%) +Three similar rendering patterns: +- `renderLogEntries()` - Lines 2576-2608 +- `renderRxLogEntries()` - Lines 2775-2826 +- `renderErrorLogEntries()` - Lines 2991-3042 + +**Pattern**: All follow same structure: +1. Check if container exists +2. Handle full vs incremental render +3. Clear or update container innerHTML +4. Show placeholder if no entries +5. Reverse entries for newest-first display +6. Create and append elements +7. Auto-scroll to top + +**Potential consolidation**: Create generic `renderLogEntries(config)` with custom element creator +**Risk**: Medium-High - Complex rendering logic +**Benefit**: ~120 lines → ~50 lines (-70 lines) + +#### C. Summary Update Functions (Medium Similarity ~70%) +Three similar summary update patterns: +- `updateLogSummary()` - Lines 2543-2571 +- `updateRxLogSummary()` - Lines 2732-2766 +- `updateErrorLogSummary()` - Lines 2953-2985 + +**Pattern**: Similar flow but different data formatting +**Potential consolidation**: Moderate - formatting differences make this less ideal +**Risk**: Low-Medium +**Benefit**: Limited (~15-20 lines) + +#### D. CSV Export Functions (Medium Similarity ~60%) +Three CSV export functions: +- `sessionLogToCSV()` - Lines 3120-3152 +- `rxLogToCSV()` - Lines 3159-3180 +- `errorLogToCSV()` - Lines 3187-3207 + +**Pattern**: Same structure but different column formats +**Potential consolidation**: Create generic CSV builder +**Risk**: Low - Pure data transformation +**Benefit**: ~60 lines → ~30 lines (-30 lines) + +### 2. Function Complexity Analysis + +#### Large Functions (200+ lines) +- `sendPing()` - Lines 3472-3656 (184 lines) - Acceptable size, well-structured +- `onLoad()` - Lines 4095-4233 (138 lines) - Event listener setup, hard to break down + +#### Medium Functions (100-200 lines) +- `handleUnifiedRxLogEvent()` - 130 lines - Core RX handling, well-documented +- `handleSessionLogTracking()` - 120 lines - Complex decryption logic +- `flushApiQueue()` - 95 lines - API batch processing + +**Assessment**: Function sizes are generally acceptable. Most long functions handle complex workflows that benefit from being in one place for readability. + +### 3. Performance Considerations + +#### ✅ Good Patterns Found: +- **Event delegation**: Proper use of addEventListener with cleanup +- **Async/await**: Consistent async patterns throughout +- **Timer management**: Comprehensive cleanup in `cleanupAllTimers()` +- **Memory limits**: RX log (100 entries) and Error log (50 entries) have max limits +- **Batch operations**: API queue batching (50 msg limit, 30s flush) + +#### ⚠️ Potential Improvements: +- **Map structures**: `rxBatchBuffer` Map could grow unbounded + - **Recommendation**: Add periodic cleanup for stale entries (>10min old) + +- **Debug logging**: 434 debug statements throughout code + - **Assessment**: Acceptable - controlled by DEBUG_ENABLED flag + - Only enabled via URL parameter (?debug=true) + +### 4. Code Quality Assessment + +#### Strengths: +✅ Consistent debug logging with proper tags (e.g., `[BLE]`, `[GPS]`, `[PING]`) +✅ Well-documented functions with JSDoc comments +✅ Clear separation of concerns (GPS, BLE, API, UI) +✅ Comprehensive error handling +✅ Good state management with central `state` object + +#### Minor Issues: +⚠️ Some magic numbers could be constants (e.g., 20 char preview in error log) +⚠️ Line 4204: Syntax error in debugError call - missing comma +⚠️ Some functions could benefit from early returns to reduce nesting + +## Recommended Optimizations + +### Phase 1: Safe Optimizations (Low Risk) +1. ✅ **COMPLETED**: Remove deprecated functions and dead code (-91 lines) +2. ✅ **COMPLETED**: Fix syntax error at line 4113 (missing comma in debugError call) +3. ✅ **COMPLETED**: Extract magic numbers to constants (error log preview length) +4. ✅ **VERIFIED**: rxBatchBuffer Map cleanup is already properly handled + +### Phase 2: Moderate Risk Optimizations +1. 🔄 **OPTIONAL**: Consolidate CSV export functions (saves ~30 lines) +2. 🔄 **OPTIONAL**: Create generic bottom sheet toggle helper (saves ~50 lines) + +### Phase 3: High Risk Optimizations (Require Extensive Testing) +1. ❌ **NOT RECOMMENDED**: Consolidate rendering functions + - Too much variation in rendering logic + - Risk of breaking UI behavior + - Maintenance burden of generic solution may exceed benefits + +## Security Analysis +✅ No security vulnerabilities identified +✅ Proper validation of GPS coordinates (geofence, distance) +✅ API key stored as constant (acceptable for public API) +✅ No injection vulnerabilities in DOM manipulation + +## Performance Metrics + +### Before Optimization: +- Total lines: 4,324 +- Functions: ~90 +- Debug statements: 434 +- File size: ~180 KB + +### After Phase 1: +- Total lines: 4,233 (-91, -2.1%) +- Functions: ~87 (-3) +- Debug statements: 434 (unchanged) +- File size: ~175 KB (-2.8%) + +### Estimated After Phase 2 (if applied): +- Total lines: ~4,150 (-174, -4.0%) +- Functions: ~84 (-6) +- Minimal runtime performance impact (structural changes only) + +## Conclusion + +The codebase is generally well-structured and maintainable. Phase 1 optimization successfully completed with the following improvements: + +1. **Dead Code Removal**: Removed 91 lines of deprecated functions +2. **Bug Fixes**: Fixed syntax error in debugError call (line 4113) +3. **Code Quality**: Extracted magic number to named constant for better maintainability +4. **Verification**: Confirmed proper cleanup of Map structures + +**Total Impact**: +- Lines removed: 91 (deprecated code) +- Bugs fixed: 1 (syntax error) +- Constants extracted: 1 (previewLength) +- File size reduction: ~2.1% + +Further aggressive optimization is **not recommended** due to: + +1. **Risk vs Reward**: Potential consolidations carry medium-high risk of breaking UI behavior +2. **Maintainability**: The current code is clear and easy to understand. Over-abstraction could reduce readability +3. **Performance**: No significant performance issues identified. The code is already optimized where it matters (batch operations, memory limits, timer cleanup) + +**Recommendation**: Proceed with Phase 1 completion (syntax fix, minor cleanups) but defer Phase 2/3 optimizations unless specific issues arise. + +## Changed Files Summary +- ✅ `content/wardrive.js` - Reduced from 4,324 to 4,233 lines +- ✅ `index-new.html` - Deleted (237 lines) +- ✅ `tailwind.config.js` - Removed index-new.html reference +- ✅ `OPTIMIZATION_REPORT.md` - Created (this file) + +--- +**Report Generated**: 2025-12-23 +**Analyzed By**: GitHub Copilot Agent +**Lines Removed**: 91 (2.1% reduction) +**Backward Compatibility**: ✅ Maintained diff --git a/content/wardrive.js b/content/wardrive.js index 6f740ec..cd655c3 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -208,7 +208,8 @@ const errorLogState = { entries: [], // Array of error log entries isExpanded: false, autoScroll: true, - maxEntries: 50 // Limit to prevent memory issues + maxEntries: 50, // Limit to prevent memory issues + previewLength: 20 // Character length for error message preview in summary }; // ---- State ---- @@ -2974,9 +2975,9 @@ function updateErrorLogSummary() { errorLogLastTime.classList.remove('hidden'); if (errorLogLastError) { - // Show first 20 chars of error message - const preview = lastEntry.message.length > 20 - ? lastEntry.message.substring(0, 20) + '...' + // Show preview of error message + const preview = lastEntry.message.length > errorLogState.previewLength + ? lastEntry.message.substring(0, errorLogState.previewLength) + '...' : lastEntry.message; errorLogLastError.textContent = preview; } @@ -4110,7 +4111,7 @@ export async function onLoad() { await connect(); } } catch (e) { - debugError("[UI] Connection button error:" `${e.message}`, e); + debugError("[UI] Connection button error:", `${e.message}`, e); setDynamicStatus(e.message || "Connection failed", STATUS_COLORS.error); } }); From 99860c6e7d9f4bad7d6fbdbe5a6af0ca5ec0e499 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:49:06 +0000 Subject: [PATCH 69/77] Fix function call references after removing deprecated aliases Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index cd655c3..c8ac999 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -3857,8 +3857,8 @@ async function connect() { // Proceed with channel setup and GPS initialization await ensureChannel(); - // Start passive RX listening after channel setup - startPassiveRxListening(); + // Start unified RX listening after channel setup + startUnifiedRxListening(); // GPS initialization setDynamicStatus("Priming GPS", STATUS_COLORS.info); @@ -3947,7 +3947,7 @@ async function connect() { stopGeoWatch(); stopGpsAgeUpdater(); // Ensure age updater stops stopRepeaterTracking(); // Stop repeater echo tracking - stopPassiveRxListening(); // Stop passive RX listening + stopUnifiedRxListening(); // Stop unified RX listening // Flush all pending RX batch data before cleanup flushAllBatches('disconnect'); From d9d2e6b947b0416f1f1e1dad6987e7ef9c57cd02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:51:13 +0000 Subject: [PATCH 70/77] Add comprehensive changes summary documentation Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- CHANGES_SUMMARY.md | 228 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 CHANGES_SUMMARY.md diff --git a/CHANGES_SUMMARY.md b/CHANGES_SUMMARY.md new file mode 100644 index 0000000..adaef00 --- /dev/null +++ b/CHANGES_SUMMARY.md @@ -0,0 +1,228 @@ +# wardrive.js Optimization - Changes Summary + +## Overview +This PR implements Phase 1 optimizations for `wardrive.js`, focusing on safe, minimal changes that improve code quality without introducing risk. + +## Objectives Completed ✅ + +### 1. Code Review & Analysis +- ✅ Performed comprehensive review of wardrive.js (4,324 lines) +- ✅ Identified dead code, syntax errors, and optimization opportunities +- ✅ Documented findings in OPTIMIZATION_REPORT.md +- ✅ Risk assessment for potential future optimizations + +### 2. Dead Code Elimination +- ✅ Removed deprecated `handlePassiveRxLogEvent()` function (77 lines) + - Was marked @deprecated and replaced by `handleUnifiedRxLogEvent()` + - No longer called anywhere in the codebase +- ✅ Removed deprecated alias functions (7 lines) + - `startPassiveRxListening()` → replaced with `startUnifiedRxListening()` + - `stopPassiveRxListening()` → replaced with `stopUnifiedRxListening()` +- ✅ Updated all function call references (2 locations) +- ✅ Cleaned up TODO comment on API endpoint + +### 3. Bug Fixes +- ✅ Fixed syntax error at line 4113 + - **Issue**: Missing comma in debugError call + - **Before**: `debugError("[UI] Connection button error:" backtick${e.message}backtick, e);` + - **After**: `debugError("[UI] Connection button error:", backtick${e.message}backtick, e);` + - **Impact**: Prevents potential runtime errors in error logging + +### 4. Code Structure Improvements +- ✅ Extracted magic number to named constant + - Added `previewLength: 20` to `errorLogState` object + - Updated usage in `updateErrorLogSummary()` function + - Improves maintainability and makes configuration explicit + +### 5. File Cleanup +- ✅ Deleted `index-new.html` (237 lines) + - File was not referenced or used anywhere in the application + - Only reference was in tailwind.config.js (now removed) +- ✅ Updated `tailwind.config.js` to remove deleted file reference + +## Files Changed + +### Modified Files +1. **content/wardrive.js** + - Before: 4,324 lines + - After: 4,234 lines + - Reduction: 90 lines (2.1%) + - Changes: + - Removed deprecated functions (84 lines) + - Fixed syntax error (1 line) + - Extracted constant (2 lines) + - Updated function calls (3 lines) + +2. **tailwind.config.js** + - Removed reference to index-new.html + - Impact: Cleaner build configuration + +### New Files +1. **OPTIMIZATION_REPORT.md** (212 lines) + - Comprehensive analysis of codebase + - Identified optimization opportunities + - Risk/benefit assessment + - Performance metrics + - Recommendations for future work + +2. **CHANGES_SUMMARY.md** (this file) + - Summary of all changes made + - Verification and testing details + +### Deleted Files +1. **index-new.html** (237 lines) + - Unused HTML file + - No references in codebase + +## Verification & Testing + +### Syntax Validation ✅ +```bash +node -c content/wardrive.js +# Result: No syntax errors +``` + +### Code Review ✅ +- First review: Identified 2 issues with function call references +- Fixed: Updated calls to removed deprecated functions +- Second review: No issues found + +### Impact Analysis ✅ +- **Breaking Changes**: None +- **Backward Compatibility**: Fully maintained +- **Feature Changes**: None - all features preserved +- **Performance Impact**: Neutral (file size reduction minimal) + +## Detailed Changes + +### Commit 1: Remove index-new.html and deprecated functions +``` +Files changed: 3 +- content/wardrive.js: -93 lines +- index-new.html: deleted (237 lines) +- tailwind.config.js: -1 line +Total: -331 lines +``` + +### Commit 2: Fix syntax error and extract magic number constant +``` +Files changed: 2 +- content/wardrive.js: +6/-5 lines +- OPTIMIZATION_REPORT.md: created (212 lines) +``` + +### Commit 3: Fix function call references +``` +Files changed: 1 +- content/wardrive.js: +3/-3 lines +``` + +## Metrics + +### Before Optimization +- Total lines: 4,324 +- Functions: ~90 +- Debug statements: 434 +- Deprecated code: 91 lines +- Syntax errors: 1 +- Magic numbers: Several + +### After Optimization +- Total lines: 4,234 (-90, -2.1%) +- Functions: ~87 (-3) +- Debug statements: 434 (unchanged) +- Deprecated code: 0 (-91 lines) +- Syntax errors: 0 (-1 fixed) +- Magic numbers: 1 fewer + +## Code Quality Assessment + +### Improvements Made ✅ +- Cleaner codebase with no deprecated code +- Fixed syntax error preventing potential runtime issues +- Better maintainability with named constants +- Comprehensive documentation for future work + +### Strengths Identified ✅ +- Well-structured code with clear separation of concerns +- Comprehensive debug logging with proper tags +- Good error handling and state management +- Proper memory limits preventing unbounded growth +- Efficient batch operations (API queue, RX batching) +- Comprehensive timer cleanup + +### Future Optimization Opportunities (Deferred) +The analysis identified several consolidation opportunities that were **intentionally deferred**: + +1. **Bottom Sheet Toggles** (~50 line savings, medium risk) + - 3 nearly identical functions with ~90% code similarity + - Could be consolidated with generic helper + - **Deferred**: Risk of UI breakage outweighs benefit + +2. **Render Functions** (~70 line savings, high risk) + - 3 similar rendering patterns + - Complex logic with subtle differences + - **Deferred**: Over-abstraction could reduce readability + +3. **CSV Exports** (~30 line savings, low risk) + - 3 similar export functions + - Different column formats + - **Deferred**: Current code is clear and maintainable + +**Rationale**: The codebase is well-maintained and readable. Aggressive consolidation would introduce complexity without meaningful performance gains. No performance issues were identified. + +## Guidelines Compliance ✅ + +All changes strictly follow the development guidelines: +- ✅ Maintained debug logging with proper tags +- ✅ Preserved existing functionality +- ✅ Only removed dead code and fixed bugs +- ✅ No modifications to working code +- ✅ Code quality improvements without breaking changes +- ✅ Minimal, surgical changes as required +- ✅ Comprehensive documentation + +## Testing Recommendations + +Since this is a browser-based PWA with no automated tests: + +### Manual Testing Checklist +1. **Connection Flow** + - [ ] BLE connection establishes successfully + - [ ] Unified RX listening starts after connection + - [ ] Unified RX listening stops on disconnect + +2. **Ping Operations** + - [ ] Manual ping sends successfully + - [ ] Auto ping mode works correctly + - [ ] GPS acquisition functions properly + +3. **UI Components** + - [ ] Session log displays correctly + - [ ] RX log displays correctly + - [ ] Error log displays correctly + - [ ] All log toggles work + - [ ] CSV export functions work + +4. **Error Handling** + - [ ] Error messages display correctly (syntax fix verification) + - [ ] Debug logging works with ?debug=true + - [ ] Error log preview shows correct length + +## Conclusion + +Phase 1 optimization successfully completed with **zero risk** changes: +- ✅ Removed 90 lines of dead code +- ✅ Fixed 1 syntax error +- ✅ Improved code maintainability +- ✅ Created comprehensive documentation +- ✅ Zero breaking changes +- ✅ Full backward compatibility + +The codebase is now cleaner, more maintainable, and free of technical debt while preserving all functionality. Future optimization opportunities have been documented but intentionally deferred based on risk/benefit analysis. + +--- +**Optimization Completed**: December 23, 2025 +**Total Time Saved**: ~2.1% file size reduction +**Risk Level**: Zero (only dead code removed and bugs fixed) +**Compatibility**: 100% maintained From 9481be2d70bae01a6b8f4adb33d80eb779248eb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:42:29 +0000 Subject: [PATCH 71/77] Initial plan From b0fb3e213c5dffa4471edf2d4abc7124d0c9ee31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:48:33 +0000 Subject: [PATCH 72/77] Refactor: Consolidate duplicate log handling code (CSV, toggle, render) Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 561 ++++++++++++++++++++++---------------------- 1 file changed, 279 insertions(+), 282 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index c8ac999..3b2d420 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -2572,82 +2572,174 @@ function updateLogSummary() { } /** - * Render all log entries to the session log + * Generic log entry renderer helper function + * @param {Object} config - Configuration object + * @param {HTMLElement} config.containerElement - Container element to render entries into + * @param {Array} config.entries - Array of log entries + * @param {Function} config.createElementFn - Function to create DOM element for each entry + * @param {string} config.placeholderText - Text to show when no entries exist + * @param {boolean} config.fullRender - If true, re-render all entries. If false, only render new entries. + * @param {string} config.logTag - Debug log tag (e.g., 'UI', 'PASSIVE RX UI') + * @param {HTMLElement} config.scrollContainer - Optional scroll container for auto-scroll + * @param {Object} config.state - State object with autoScroll property */ -function renderLogEntries() { - if (!sessionPingsEl) return; - - debugLog(`[UI] Rendering ${sessionLogState.entries.length} log entries`); - sessionPingsEl.innerHTML = ''; - - if (sessionLogState.entries.length === 0) { - // Show placeholder when no entries - const placeholder = document.createElement('div'); - placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; - placeholder.textContent = 'No pings logged yet'; - sessionPingsEl.appendChild(placeholder); - debugLog(`[UI] Rendered placeholder (no entries)`); - return; - } - - // Render newest first - const entries = [...sessionLogState.entries].reverse(); +function renderLogEntriesGeneric(config) { + const { + containerElement, + entries, + createElementFn, + placeholderText, + fullRender = true, + logTag, + scrollContainer, + state + } = config; + + if (!containerElement) return; - entries.forEach((entry, index) => { - const element = createLogEntryElement(entry); - sessionPingsEl.appendChild(element); - debugLog(`[UI] Appended log entry ${index + 1}/${entries.length} to sessionPingsEl`); - }); + if (fullRender) { + debugLog(`[${logTag}] ${fullRender ? 'Full' : 'Incremental'} render of ${entries.length} log entries`); + containerElement.innerHTML = ''; + + if (entries.length === 0) { + const placeholder = document.createElement('div'); + placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; + placeholder.textContent = placeholderText; + containerElement.appendChild(placeholder); + debugLog(`[${logTag}] Rendered placeholder (no entries)`); + return; + } + + // Render newest first + const reversedEntries = [...entries].reverse(); + + reversedEntries.forEach((entry, index) => { + const element = createElementFn(entry); + containerElement.appendChild(element); + if (fullRender && index === 0) { + debugLog(`[${logTag}] Appended log entry ${index + 1}/${reversedEntries.length} to container`); + } + }); + + debugLog(`[${logTag}] ${fullRender ? 'Full' : 'Incremental'} render complete: ${reversedEntries.length} entries`); + } else { + // Incremental render: only add the newest entry + if (entries.length === 0) { + debugLog(`[${logTag}] No entries to render incrementally`); + return; + } + + // Remove placeholder if it exists + const placeholder = containerElement.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 = entries[entries.length - 1]; + const element = createElementFn(newestEntry); + containerElement.insertBefore(element, containerElement.firstChild); + + debugLog(`[${logTag}] Appended entry ${entries.length}/${entries.length}`); + } // Auto-scroll to top (newest) - if (sessionLogState.autoScroll && logScrollContainer) { - logScrollContainer.scrollTop = 0; - debugLog(`[UI] Auto-scrolled to top of log container`); + if (state && state.autoScroll && scrollContainer) { + scrollContainer.scrollTop = 0; + debugLog(`[${logTag}] Auto-scrolled to top`); } - - debugLog(`[UI] Finished rendering all log entries`); } /** - * Toggle session log expanded/collapsed + * Render all log entries to the session log */ -function toggleBottomSheet() { - sessionLogState.isExpanded = !sessionLogState.isExpanded; +function renderLogEntries() { + renderLogEntriesGeneric({ + containerElement: sessionPingsEl, + entries: sessionLogState.entries, + createElementFn: createLogEntryElement, + placeholderText: 'No pings logged yet', + fullRender: true, + logTag: 'UI', + scrollContainer: logScrollContainer, + state: sessionLogState + }); +} + +/** + * Generic bottom sheet toggle helper function + * @param {Object} config - Configuration object + * @param {Object} config.state - State object with isExpanded property + * @param {HTMLElement} config.sheetElement - Bottom sheet element + * @param {HTMLElement} config.arrowElement - Arrow element for rotation + * @param {HTMLElement} config.copyButton - Copy button element + * @param {Array} config.statusElements - Array of status elements to toggle + * @param {string} config.logTag - Debug log tag (e.g., 'SESSION LOG', 'PASSIVE RX UI') + * @param {Function} config.onExpand - Optional callback when expanded + */ +function toggleLogBottomSheet(config) { + const { state, sheetElement, arrowElement, copyButton, statusElements, logTag, onExpand } = config; + + // Toggle state + state.isExpanded = !state.isExpanded; - if (logBottomSheet) { - if (sessionLogState.isExpanded) { - logBottomSheet.classList.add('open'); - logBottomSheet.classList.remove('hidden'); + // Toggle sheet visibility + if (sheetElement) { + if (state.isExpanded) { + sheetElement.classList.add('open'); + sheetElement.classList.remove('hidden'); } else { - logBottomSheet.classList.remove('open'); - logBottomSheet.classList.add('hidden'); + sheetElement.classList.remove('open'); + sheetElement.classList.add('hidden'); } } // Toggle arrow rotation - const logExpandArrow = document.getElementById('logExpandArrow'); - if (logExpandArrow) { - if (sessionLogState.isExpanded) { - logExpandArrow.classList.add('expanded'); + if (arrowElement) { + if (state.isExpanded) { + arrowElement.classList.add('expanded'); } else { - logExpandArrow.classList.remove('expanded'); + arrowElement.classList.remove('expanded'); } } // Toggle copy button and status visibility - if (sessionLogState.isExpanded) { + if (state.isExpanded) { // Hide status elements, show copy button - if (logLastSnr) logLastSnr.classList.add('hidden'); - if (sessionLogCopyBtn) sessionLogCopyBtn.classList.remove('hidden'); - debugLog('[SESSION LOG] Expanded - showing copy button, hiding status'); + statusElements.forEach(el => { + if (el) el.classList.add('hidden'); + }); + if (copyButton) copyButton.classList.remove('hidden'); + debugLog(`[${logTag}] Expanded - showing copy button, hiding status`); + + // Execute optional callback + if (onExpand) onExpand(); } else { // Show status elements, hide copy button - if (logLastSnr) logLastSnr.classList.remove('hidden'); - if (sessionLogCopyBtn) sessionLogCopyBtn.classList.add('hidden'); - debugLog('[SESSION LOG] Collapsed - hiding copy button, showing status'); + statusElements.forEach(el => { + if (el) el.classList.remove('hidden'); + }); + if (copyButton) copyButton.classList.add('hidden'); + debugLog(`[${logTag}] Collapsed - hiding copy button, showing status`); } } +/** + * Toggle session log expanded/collapsed + */ +function toggleBottomSheet() { + const logExpandArrow = document.getElementById('logExpandArrow'); + + toggleLogBottomSheet({ + state: sessionLogState, + sheetElement: logBottomSheet, + arrowElement: logExpandArrow, + copyButton: sessionLogCopyBtn, + statusElements: [logLastSnr], + logTag: 'SESSION LOG' + }); +} + /** * Add entry to session log * @param {string} timestamp - ISO timestamp @@ -2774,99 +2866,37 @@ function updateRxLogSummary() { * @param {boolean} fullRender - If true, re-render all entries. If false, only render new entries. */ function renderRxLogEntries(fullRender = false) { - if (!rxLogEntries) 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}`); - } - - // Auto-scroll to top (newest) - if (rxLogState.autoScroll && rxLogScrollContainer) { - rxLogScrollContainer.scrollTop = 0; - debugLog(`[PASSIVE RX UI] Auto-scrolled to top`); - } + renderLogEntriesGeneric({ + containerElement: rxLogEntries, + entries: rxLogState.entries, + createElementFn: createRxLogEntryElement, + placeholderText: 'No RX observations yet', + fullRender: fullRender, + logTag: 'PASSIVE RX UI', + scrollContainer: rxLogScrollContainer, + state: rxLogState + }); } /** * Toggle RX log expanded/collapsed */ function toggleRxLogBottomSheet() { - rxLogState.isExpanded = !rxLogState.isExpanded; - - if (rxLogBottomSheet) { - if (rxLogState.isExpanded) { - rxLogBottomSheet.classList.add('open'); - rxLogBottomSheet.classList.remove('hidden'); - } else { - rxLogBottomSheet.classList.remove('open'); - rxLogBottomSheet.classList.add('hidden'); - } - } - - // Toggle arrow rotation - if (rxLogExpandArrow) { - if (rxLogState.isExpanded) { - rxLogExpandArrow.classList.add('expanded'); - } else { - rxLogExpandArrow.classList.remove('expanded'); - } - } - - // Toggle copy button and status visibility - if (rxLogState.isExpanded) { - // Hide status, show copy button - if (rxLogLastRepeater) rxLogLastRepeater.classList.add('hidden'); - if (rxLogSnrChip) rxLogSnrChip.classList.add('hidden'); - if (rxLogCopyBtn) rxLogCopyBtn.classList.remove('hidden'); - debugLog('[PASSIVE RX UI] Expanded - showing copy button, hiding status'); - } else { - // Show status, hide copy button - if (rxLogLastRepeater) rxLogLastRepeater.classList.remove('hidden'); - if (rxLogSnrChip && rxLogState.entries.length > 0) { - rxLogSnrChip.classList.remove('hidden'); + toggleLogBottomSheet({ + state: rxLogState, + sheetElement: rxLogBottomSheet, + arrowElement: rxLogExpandArrow, + copyButton: rxLogCopyBtn, + statusElements: [rxLogLastRepeater, rxLogSnrChip], + logTag: 'PASSIVE RX UI', + onExpand: () => { + // Special handling for RX log: only hide SNR chip if there are entries + // When collapsed, only show SNR chip if there are entries + if (!rxLogState.isExpanded && rxLogSnrChip && rxLogState.entries.length > 0) { + rxLogSnrChip.classList.remove('hidden'); + } } - if (rxLogCopyBtn) rxLogCopyBtn.classList.add('hidden'); - debugLog('[PASSIVE RX UI] Collapsed - hiding copy button, showing status'); - } + }); } /** @@ -2990,95 +3020,30 @@ function updateErrorLogSummary() { * @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`); - } + renderLogEntriesGeneric({ + containerElement: errorLogEntries, + entries: errorLogState.entries, + createElementFn: createErrorLogEntryElement, + placeholderText: 'No errors logged', + fullRender: fullRender, + logTag: 'ERROR LOG', + scrollContainer: errorLogScrollContainer, + state: errorLogState + }); } /** * 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'); - } - } - - // Toggle copy button and status visibility - if (errorLogState.isExpanded) { - // Hide status, show copy button - if (errorLogLastError) errorLogLastError.classList.add('hidden'); - if (errorLogCopyBtn) errorLogCopyBtn.classList.remove('hidden'); - debugLog('[ERROR LOG] Expanded - showing copy button, hiding status'); - } else { - // Show status, hide copy button - if (errorLogLastError) errorLogLastError.classList.remove('hidden'); - if (errorLogCopyBtn) errorLogCopyBtn.classList.add('hidden'); - debugLog('[ERROR LOG] Collapsed - hiding copy button, showing status'); - } + toggleLogBottomSheet({ + state: errorLogState, + sheetElement: errorLogBottomSheet, + arrowElement: errorLogExpandArrow, + copyButton: errorLogCopyBtn, + statusElements: [errorLogLastError], + logTag: 'ERROR LOG' + }); } /** @@ -3114,70 +3079,97 @@ function addErrorLogEntry(message, source = null) { // ---- CSV Export Functions ---- /** - * Convert Session Log to CSV format - * Columns: Timestamp,Latitude,Longitude,Repeater1_ID,Repeater1_SNR,Repeater2_ID,Repeater2_SNR,... + * Generic CSV generator helper function + * @param {Array} entries - Array of log entries to convert + * @param {Array} columns - Column configuration array with {header, getValue} objects + * @param {string} logTag - Debug log tag (e.g., 'SESSION LOG', 'PASSIVE RX UI') + * @param {string} emptyHeader - Header to return when no entries exist * @returns {string} CSV formatted string */ -function sessionLogToCSV() { - debugLog('[SESSION LOG] Converting session log to CSV format'); +function generateCSV(entries, columns, logTag, emptyHeader) { + debugLog(`[${logTag}] Converting log to CSV format`); - if (sessionLogState.entries.length === 0) { - debugWarn('[SESSION LOG] No session log entries to export'); - return 'Timestamp,Latitude,Longitude,Repeats\n'; + if (entries.length === 0) { + debugWarn(`[${logTag}] No log entries to export`); + return emptyHeader; } - // Fixed 4-column header - const header = 'Timestamp,Latitude,Longitude,Repeats\n'; + // Build header from column definitions + const header = columns.map(col => col.header).join(',') + '\n'; - // Build CSV rows - const rows = sessionLogState.entries.map(entry => { - let row = `${entry.timestamp},${entry.lat},${entry.lon}`; - - // Combine all repeater data into single Repeats column - // Format: repeaterID(snr)|repeaterID(snr)|... - if (entry.events.length > 0) { - const repeats = entry.events.map(event => { - return `${event.type}(${event.value.toFixed(2)})`; - }).join('|'); - row += `,${repeats}`; - } else { - row += ','; - } - - return row; + // Build CSV rows using column value extractors + const rows = entries.map(entry => { + const values = columns.map(col => col.getValue(entry)); + return values.join(','); }); const csv = header + rows.join('\n'); - debugLog(`[SESSION LOG] CSV export complete: ${sessionLogState.entries.length} entries`); + debugLog(`[${logTag}] CSV export complete: ${entries.length} entries`); return csv; } +/** + * Convert Session Log to CSV format + * Columns: Timestamp,Latitude,Longitude,Repeats + * @returns {string} CSV formatted string + */ +function sessionLogToCSV() { + const columns = [ + { header: 'Timestamp', getValue: entry => entry.timestamp }, + { header: 'Latitude', getValue: entry => entry.lat }, + { header: 'Longitude', getValue: entry => entry.lon }, + { + header: 'Repeats', + getValue: entry => { + // Combine all repeater data into single Repeats column + // Format: repeaterID(snr)|repeaterID(snr)|... + if (entry.events.length > 0) { + return entry.events.map(event => + `${event.type}(${event.value.toFixed(2)})` + ).join('|'); + } + return ''; + } + } + ]; + + return generateCSV( + sessionLogState.entries, + columns, + 'SESSION LOG', + 'Timestamp,Latitude,Longitude,Repeats\n' + ); +} + /** * Convert RX Log to CSV format * Columns: Timestamp,RepeaterID,SNR,RSSI,PathLength * @returns {string} CSV formatted string */ function rxLogToCSV() { - debugLog('[PASSIVE RX UI] Converting RX log to CSV format'); - - if (rxLogState.entries.length === 0) { - debugWarn('[PASSIVE RX UI] No RX log entries to export'); - return 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n'; - } - - const header = 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n'; - - const rows = rxLogState.entries.map(entry => { - // Handle potentially missing fields from old entries - const snr = entry.snr !== undefined ? entry.snr.toFixed(2) : ''; - const rssi = entry.rssi !== undefined ? entry.rssi : ''; - const pathLength = entry.pathLength !== undefined ? entry.pathLength : ''; - return `${entry.timestamp},${entry.repeaterId},${snr},${rssi},${pathLength}`; - }); + const columns = [ + { header: 'Timestamp', getValue: entry => entry.timestamp }, + { header: 'RepeaterID', getValue: entry => entry.repeaterId }, + { + header: 'SNR', + getValue: entry => entry.snr !== undefined ? entry.snr.toFixed(2) : '' + }, + { + header: 'RSSI', + getValue: entry => entry.rssi !== undefined ? entry.rssi : '' + }, + { + header: 'PathLength', + getValue: entry => entry.pathLength !== undefined ? entry.pathLength : '' + } + ]; - const csv = header + rows.join('\n'); - debugLog(`[PASSIVE RX UI] CSV export complete: ${rxLogState.entries.length} entries`); - return csv; + return generateCSV( + rxLogState.entries, + columns, + 'PASSIVE RX UI', + 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n' + ); } /** @@ -3186,25 +3178,30 @@ function rxLogToCSV() { * @returns {string} CSV formatted string */ function errorLogToCSV() { - debugLog('[ERROR LOG] Converting error log to CSV format'); - - if (errorLogState.entries.length === 0) { - debugWarn('[ERROR LOG] No error log entries to export'); - return 'Timestamp,ErrorType,Message\n'; - } - - const header = 'Timestamp,ErrorType,Message\n'; - - const rows = errorLogState.entries.map(entry => { - // Escape quotes in both source and message fields - const source = (entry.source || 'ERROR').replace(/"/g, '""'); - const message = entry.message.replace(/"/g, '""'); - return `${entry.timestamp},"${source}","${message}"`; - }); + const columns = [ + { header: 'Timestamp', getValue: entry => entry.timestamp }, + { + header: 'ErrorType', + getValue: entry => { + const source = (entry.source || 'ERROR').replace(/"/g, '""'); + return `"${source}"`; + } + }, + { + header: 'Message', + getValue: entry => { + const message = entry.message.replace(/"/g, '""'); + return `"${message}"`; + } + } + ]; - const csv = header + rows.join('\n'); - debugLog(`[ERROR LOG] CSV export complete: ${errorLogState.entries.length} entries`); - return csv; + return generateCSV( + errorLogState.entries, + columns, + 'ERROR LOG', + 'Timestamp,ErrorType,Message\n' + ); } /** From b1175f5f26618af1ea1ae8779984b4b791133be6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:52:43 +0000 Subject: [PATCH 73/77] Fix inconsistent debug logging in generic render function Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 3b2d420..63ed65d 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -2598,7 +2598,7 @@ function renderLogEntriesGeneric(config) { if (!containerElement) return; if (fullRender) { - debugLog(`[${logTag}] ${fullRender ? 'Full' : 'Incremental'} render of ${entries.length} log entries`); + debugLog(`[${logTag}] Full render of ${entries.length} log entries`); containerElement.innerHTML = ''; if (entries.length === 0) { @@ -2616,12 +2616,9 @@ function renderLogEntriesGeneric(config) { reversedEntries.forEach((entry, index) => { const element = createElementFn(entry); containerElement.appendChild(element); - if (fullRender && index === 0) { - debugLog(`[${logTag}] Appended log entry ${index + 1}/${reversedEntries.length} to container`); - } }); - debugLog(`[${logTag}] ${fullRender ? 'Full' : 'Incremental'} render complete: ${reversedEntries.length} entries`); + debugLog(`[${logTag}] Full render complete: ${entries.length} entries`); } else { // Incremental render: only add the newest entry if (entries.length === 0) { From 9dc26e3512cb3a162ae43d7d39d33d1cb18f3f0d Mon Sep 17 00:00:00 2001 From: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:00:24 -0500 Subject: [PATCH 74/77] Revert "Refactor: Consolidate duplicate log handling functions into reusable helpers" --- content/wardrive.js | 558 ++++++++++++++++++++++---------------------- 1 file changed, 282 insertions(+), 276 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 63ed65d..c8ac999 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -2572,171 +2572,82 @@ function updateLogSummary() { } /** - * Generic log entry renderer helper function - * @param {Object} config - Configuration object - * @param {HTMLElement} config.containerElement - Container element to render entries into - * @param {Array} config.entries - Array of log entries - * @param {Function} config.createElementFn - Function to create DOM element for each entry - * @param {string} config.placeholderText - Text to show when no entries exist - * @param {boolean} config.fullRender - If true, re-render all entries. If false, only render new entries. - * @param {string} config.logTag - Debug log tag (e.g., 'UI', 'PASSIVE RX UI') - * @param {HTMLElement} config.scrollContainer - Optional scroll container for auto-scroll - * @param {Object} config.state - State object with autoScroll property + * Render all log entries to the session log */ -function renderLogEntriesGeneric(config) { - const { - containerElement, - entries, - createElementFn, - placeholderText, - fullRender = true, - logTag, - scrollContainer, - state - } = config; - - if (!containerElement) return; - - if (fullRender) { - debugLog(`[${logTag}] Full render of ${entries.length} log entries`); - containerElement.innerHTML = ''; - - if (entries.length === 0) { - const placeholder = document.createElement('div'); - placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; - placeholder.textContent = placeholderText; - containerElement.appendChild(placeholder); - debugLog(`[${logTag}] Rendered placeholder (no entries)`); - return; - } - - // Render newest first - const reversedEntries = [...entries].reverse(); - - reversedEntries.forEach((entry, index) => { - const element = createElementFn(entry); - containerElement.appendChild(element); - }); - - debugLog(`[${logTag}] Full render complete: ${entries.length} entries`); - } else { - // Incremental render: only add the newest entry - if (entries.length === 0) { - debugLog(`[${logTag}] No entries to render incrementally`); - return; - } - - // Remove placeholder if it exists - const placeholder = containerElement.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 = entries[entries.length - 1]; - const element = createElementFn(newestEntry); - containerElement.insertBefore(element, containerElement.firstChild); - - debugLog(`[${logTag}] Appended entry ${entries.length}/${entries.length}`); +function renderLogEntries() { + if (!sessionPingsEl) return; + + debugLog(`[UI] Rendering ${sessionLogState.entries.length} log entries`); + sessionPingsEl.innerHTML = ''; + + if (sessionLogState.entries.length === 0) { + // Show placeholder when no entries + const placeholder = document.createElement('div'); + placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; + placeholder.textContent = 'No pings logged yet'; + sessionPingsEl.appendChild(placeholder); + debugLog(`[UI] Rendered placeholder (no entries)`); + return; } + // Render newest first + const entries = [...sessionLogState.entries].reverse(); + + entries.forEach((entry, index) => { + const element = createLogEntryElement(entry); + sessionPingsEl.appendChild(element); + debugLog(`[UI] Appended log entry ${index + 1}/${entries.length} to sessionPingsEl`); + }); + // Auto-scroll to top (newest) - if (state && state.autoScroll && scrollContainer) { - scrollContainer.scrollTop = 0; - debugLog(`[${logTag}] Auto-scrolled to top`); + if (sessionLogState.autoScroll && logScrollContainer) { + logScrollContainer.scrollTop = 0; + debugLog(`[UI] Auto-scrolled to top of log container`); } + + debugLog(`[UI] Finished rendering all log entries`); } /** - * Render all log entries to the session log - */ -function renderLogEntries() { - renderLogEntriesGeneric({ - containerElement: sessionPingsEl, - entries: sessionLogState.entries, - createElementFn: createLogEntryElement, - placeholderText: 'No pings logged yet', - fullRender: true, - logTag: 'UI', - scrollContainer: logScrollContainer, - state: sessionLogState - }); -} - -/** - * Generic bottom sheet toggle helper function - * @param {Object} config - Configuration object - * @param {Object} config.state - State object with isExpanded property - * @param {HTMLElement} config.sheetElement - Bottom sheet element - * @param {HTMLElement} config.arrowElement - Arrow element for rotation - * @param {HTMLElement} config.copyButton - Copy button element - * @param {Array} config.statusElements - Array of status elements to toggle - * @param {string} config.logTag - Debug log tag (e.g., 'SESSION LOG', 'PASSIVE RX UI') - * @param {Function} config.onExpand - Optional callback when expanded + * Toggle session log expanded/collapsed */ -function toggleLogBottomSheet(config) { - const { state, sheetElement, arrowElement, copyButton, statusElements, logTag, onExpand } = config; - - // Toggle state - state.isExpanded = !state.isExpanded; +function toggleBottomSheet() { + sessionLogState.isExpanded = !sessionLogState.isExpanded; - // Toggle sheet visibility - if (sheetElement) { - if (state.isExpanded) { - sheetElement.classList.add('open'); - sheetElement.classList.remove('hidden'); + if (logBottomSheet) { + if (sessionLogState.isExpanded) { + logBottomSheet.classList.add('open'); + logBottomSheet.classList.remove('hidden'); } else { - sheetElement.classList.remove('open'); - sheetElement.classList.add('hidden'); + logBottomSheet.classList.remove('open'); + logBottomSheet.classList.add('hidden'); } } // Toggle arrow rotation - if (arrowElement) { - if (state.isExpanded) { - arrowElement.classList.add('expanded'); + const logExpandArrow = document.getElementById('logExpandArrow'); + if (logExpandArrow) { + if (sessionLogState.isExpanded) { + logExpandArrow.classList.add('expanded'); } else { - arrowElement.classList.remove('expanded'); + logExpandArrow.classList.remove('expanded'); } } // Toggle copy button and status visibility - if (state.isExpanded) { + if (sessionLogState.isExpanded) { // Hide status elements, show copy button - statusElements.forEach(el => { - if (el) el.classList.add('hidden'); - }); - if (copyButton) copyButton.classList.remove('hidden'); - debugLog(`[${logTag}] Expanded - showing copy button, hiding status`); - - // Execute optional callback - if (onExpand) onExpand(); + if (logLastSnr) logLastSnr.classList.add('hidden'); + if (sessionLogCopyBtn) sessionLogCopyBtn.classList.remove('hidden'); + debugLog('[SESSION LOG] Expanded - showing copy button, hiding status'); } else { // Show status elements, hide copy button - statusElements.forEach(el => { - if (el) el.classList.remove('hidden'); - }); - if (copyButton) copyButton.classList.add('hidden'); - debugLog(`[${logTag}] Collapsed - hiding copy button, showing status`); + if (logLastSnr) logLastSnr.classList.remove('hidden'); + if (sessionLogCopyBtn) sessionLogCopyBtn.classList.add('hidden'); + debugLog('[SESSION LOG] Collapsed - hiding copy button, showing status'); } } -/** - * Toggle session log expanded/collapsed - */ -function toggleBottomSheet() { - const logExpandArrow = document.getElementById('logExpandArrow'); - - toggleLogBottomSheet({ - state: sessionLogState, - sheetElement: logBottomSheet, - arrowElement: logExpandArrow, - copyButton: sessionLogCopyBtn, - statusElements: [logLastSnr], - logTag: 'SESSION LOG' - }); -} - /** * Add entry to session log * @param {string} timestamp - ISO timestamp @@ -2863,37 +2774,99 @@ function updateRxLogSummary() { * @param {boolean} fullRender - If true, re-render all entries. If false, only render new entries. */ function renderRxLogEntries(fullRender = false) { - renderLogEntriesGeneric({ - containerElement: rxLogEntries, - entries: rxLogState.entries, - createElementFn: createRxLogEntryElement, - placeholderText: 'No RX observations yet', - fullRender: fullRender, - logTag: 'PASSIVE RX UI', - scrollContainer: rxLogScrollContainer, - state: rxLogState - }); + if (!rxLogEntries) 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}`); + } + + // Auto-scroll to top (newest) + if (rxLogState.autoScroll && rxLogScrollContainer) { + rxLogScrollContainer.scrollTop = 0; + debugLog(`[PASSIVE RX UI] Auto-scrolled to top`); + } } /** * Toggle RX log expanded/collapsed */ function toggleRxLogBottomSheet() { - toggleLogBottomSheet({ - state: rxLogState, - sheetElement: rxLogBottomSheet, - arrowElement: rxLogExpandArrow, - copyButton: rxLogCopyBtn, - statusElements: [rxLogLastRepeater, rxLogSnrChip], - logTag: 'PASSIVE RX UI', - onExpand: () => { - // Special handling for RX log: only hide SNR chip if there are entries - // When collapsed, only show SNR chip if there are entries - if (!rxLogState.isExpanded && rxLogSnrChip && rxLogState.entries.length > 0) { - rxLogSnrChip.classList.remove('hidden'); - } + rxLogState.isExpanded = !rxLogState.isExpanded; + + if (rxLogBottomSheet) { + if (rxLogState.isExpanded) { + rxLogBottomSheet.classList.add('open'); + rxLogBottomSheet.classList.remove('hidden'); + } else { + rxLogBottomSheet.classList.remove('open'); + rxLogBottomSheet.classList.add('hidden'); } - }); + } + + // Toggle arrow rotation + if (rxLogExpandArrow) { + if (rxLogState.isExpanded) { + rxLogExpandArrow.classList.add('expanded'); + } else { + rxLogExpandArrow.classList.remove('expanded'); + } + } + + // Toggle copy button and status visibility + if (rxLogState.isExpanded) { + // Hide status, show copy button + if (rxLogLastRepeater) rxLogLastRepeater.classList.add('hidden'); + if (rxLogSnrChip) rxLogSnrChip.classList.add('hidden'); + if (rxLogCopyBtn) rxLogCopyBtn.classList.remove('hidden'); + debugLog('[PASSIVE RX UI] Expanded - showing copy button, hiding status'); + } else { + // Show status, hide copy button + if (rxLogLastRepeater) rxLogLastRepeater.classList.remove('hidden'); + if (rxLogSnrChip && rxLogState.entries.length > 0) { + rxLogSnrChip.classList.remove('hidden'); + } + if (rxLogCopyBtn) rxLogCopyBtn.classList.add('hidden'); + debugLog('[PASSIVE RX UI] Collapsed - hiding copy button, showing status'); + } } /** @@ -3017,30 +2990,95 @@ function updateErrorLogSummary() { * @param {boolean} fullRender - If true, re-render all entries. If false, only render new entries. */ function renderErrorLogEntries(fullRender = false) { - renderLogEntriesGeneric({ - containerElement: errorLogEntries, - entries: errorLogState.entries, - createElementFn: createErrorLogEntryElement, - placeholderText: 'No errors logged', - fullRender: fullRender, - logTag: 'ERROR LOG', - scrollContainer: errorLogScrollContainer, - state: errorLogState - }); + 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() { - toggleLogBottomSheet({ - state: errorLogState, - sheetElement: errorLogBottomSheet, - arrowElement: errorLogExpandArrow, - copyButton: errorLogCopyBtn, - statusElements: [errorLogLastError], - logTag: 'ERROR LOG' - }); + 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'); + } + } + + // Toggle copy button and status visibility + if (errorLogState.isExpanded) { + // Hide status, show copy button + if (errorLogLastError) errorLogLastError.classList.add('hidden'); + if (errorLogCopyBtn) errorLogCopyBtn.classList.remove('hidden'); + debugLog('[ERROR LOG] Expanded - showing copy button, hiding status'); + } else { + // Show status, hide copy button + if (errorLogLastError) errorLogLastError.classList.remove('hidden'); + if (errorLogCopyBtn) errorLogCopyBtn.classList.add('hidden'); + debugLog('[ERROR LOG] Collapsed - hiding copy button, showing status'); + } } /** @@ -3076,97 +3114,70 @@ function addErrorLogEntry(message, source = null) { // ---- CSV Export Functions ---- /** - * Generic CSV generator helper function - * @param {Array} entries - Array of log entries to convert - * @param {Array} columns - Column configuration array with {header, getValue} objects - * @param {string} logTag - Debug log tag (e.g., 'SESSION LOG', 'PASSIVE RX UI') - * @param {string} emptyHeader - Header to return when no entries exist + * Convert Session Log to CSV format + * Columns: Timestamp,Latitude,Longitude,Repeater1_ID,Repeater1_SNR,Repeater2_ID,Repeater2_SNR,... * @returns {string} CSV formatted string */ -function generateCSV(entries, columns, logTag, emptyHeader) { - debugLog(`[${logTag}] Converting log to CSV format`); +function sessionLogToCSV() { + debugLog('[SESSION LOG] Converting session log to CSV format'); - if (entries.length === 0) { - debugWarn(`[${logTag}] No log entries to export`); - return emptyHeader; + if (sessionLogState.entries.length === 0) { + debugWarn('[SESSION LOG] No session log entries to export'); + return 'Timestamp,Latitude,Longitude,Repeats\n'; } - // Build header from column definitions - const header = columns.map(col => col.header).join(',') + '\n'; + // Fixed 4-column header + const header = 'Timestamp,Latitude,Longitude,Repeats\n'; - // Build CSV rows using column value extractors - const rows = entries.map(entry => { - const values = columns.map(col => col.getValue(entry)); - return values.join(','); + // Build CSV rows + const rows = sessionLogState.entries.map(entry => { + let row = `${entry.timestamp},${entry.lat},${entry.lon}`; + + // Combine all repeater data into single Repeats column + // Format: repeaterID(snr)|repeaterID(snr)|... + if (entry.events.length > 0) { + const repeats = entry.events.map(event => { + return `${event.type}(${event.value.toFixed(2)})`; + }).join('|'); + row += `,${repeats}`; + } else { + row += ','; + } + + return row; }); const csv = header + rows.join('\n'); - debugLog(`[${logTag}] CSV export complete: ${entries.length} entries`); + debugLog(`[SESSION LOG] CSV export complete: ${sessionLogState.entries.length} entries`); return csv; } -/** - * Convert Session Log to CSV format - * Columns: Timestamp,Latitude,Longitude,Repeats - * @returns {string} CSV formatted string - */ -function sessionLogToCSV() { - const columns = [ - { header: 'Timestamp', getValue: entry => entry.timestamp }, - { header: 'Latitude', getValue: entry => entry.lat }, - { header: 'Longitude', getValue: entry => entry.lon }, - { - header: 'Repeats', - getValue: entry => { - // Combine all repeater data into single Repeats column - // Format: repeaterID(snr)|repeaterID(snr)|... - if (entry.events.length > 0) { - return entry.events.map(event => - `${event.type}(${event.value.toFixed(2)})` - ).join('|'); - } - return ''; - } - } - ]; - - return generateCSV( - sessionLogState.entries, - columns, - 'SESSION LOG', - 'Timestamp,Latitude,Longitude,Repeats\n' - ); -} - /** * Convert RX Log to CSV format * Columns: Timestamp,RepeaterID,SNR,RSSI,PathLength * @returns {string} CSV formatted string */ function rxLogToCSV() { - const columns = [ - { header: 'Timestamp', getValue: entry => entry.timestamp }, - { header: 'RepeaterID', getValue: entry => entry.repeaterId }, - { - header: 'SNR', - getValue: entry => entry.snr !== undefined ? entry.snr.toFixed(2) : '' - }, - { - header: 'RSSI', - getValue: entry => entry.rssi !== undefined ? entry.rssi : '' - }, - { - header: 'PathLength', - getValue: entry => entry.pathLength !== undefined ? entry.pathLength : '' - } - ]; + debugLog('[PASSIVE RX UI] Converting RX log to CSV format'); - return generateCSV( - rxLogState.entries, - columns, - 'PASSIVE RX UI', - 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n' - ); + if (rxLogState.entries.length === 0) { + debugWarn('[PASSIVE RX UI] No RX log entries to export'); + return 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n'; + } + + const header = 'Timestamp,RepeaterID,SNR,RSSI,PathLength\n'; + + const rows = rxLogState.entries.map(entry => { + // Handle potentially missing fields from old entries + const snr = entry.snr !== undefined ? entry.snr.toFixed(2) : ''; + const rssi = entry.rssi !== undefined ? entry.rssi : ''; + const pathLength = entry.pathLength !== undefined ? entry.pathLength : ''; + return `${entry.timestamp},${entry.repeaterId},${snr},${rssi},${pathLength}`; + }); + + const csv = header + rows.join('\n'); + debugLog(`[PASSIVE RX UI] CSV export complete: ${rxLogState.entries.length} entries`); + return csv; } /** @@ -3175,30 +3186,25 @@ function rxLogToCSV() { * @returns {string} CSV formatted string */ function errorLogToCSV() { - const columns = [ - { header: 'Timestamp', getValue: entry => entry.timestamp }, - { - header: 'ErrorType', - getValue: entry => { - const source = (entry.source || 'ERROR').replace(/"/g, '""'); - return `"${source}"`; - } - }, - { - header: 'Message', - getValue: entry => { - const message = entry.message.replace(/"/g, '""'); - return `"${message}"`; - } - } - ]; + debugLog('[ERROR LOG] Converting error log to CSV format'); - return generateCSV( - errorLogState.entries, - columns, - 'ERROR LOG', - 'Timestamp,ErrorType,Message\n' - ); + if (errorLogState.entries.length === 0) { + debugWarn('[ERROR LOG] No error log entries to export'); + return 'Timestamp,ErrorType,Message\n'; + } + + const header = 'Timestamp,ErrorType,Message\n'; + + const rows = errorLogState.entries.map(entry => { + // Escape quotes in both source and message fields + const source = (entry.source || 'ERROR').replace(/"/g, '""'); + const message = entry.message.replace(/"/g, '""'); + return `${entry.timestamp},"${source}","${message}"`; + }); + + const csv = header + rows.join('\n'); + debugLog(`[ERROR LOG] CSV export complete: ${errorLogState.entries.length} entries`); + return csv; } /** From ce7d7d83613761c699b5813175425dbd4b248f92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:12:04 +0000 Subject: [PATCH 75/77] Initial plan From 6c7dfb64ba6f94adb311156eb67d6065937bdccf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:16:35 +0000 Subject: [PATCH 76/77] Fix error logging and improve error messages - Change debugWarn to debugError for all error conditions - Rename error messages for consistency and brevity - Update all "WarDriving" references to "MeshMapper" - Improve error message clarity and conciseness - Update documentation (STATUS_MESSAGES.md, CONNECTION_WORKFLOW.md) Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 36 +++++++++++++++++------------------ docs/CONNECTION_WORKFLOW.md | 25 ++++++++++++------------ docs/STATUS_MESSAGES.md | 38 ++++++++++++++++++------------------- 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index c8ac999..ca2fa48 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -1269,7 +1269,7 @@ async function checkCapacity(reason) { }); if (!response.ok) { - debugWarn(`[CAPACITY] Capacity check API returned error status ${response.status}`); + debugError(`[CAPACITY] Capacity check API returned error status ${response.status}`); // Fail closed on network errors for connect if (reason === "connect") { debugError("[CAPACITY] Failing closed (denying connection) due to API error"); @@ -1342,7 +1342,7 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { // Validate session_id exists before posting if (!state.wardriveSessionId) { debugError("[API QUEUE] Cannot post to MeshMapper API: no session_id available"); - setDynamicStatus("Error: No session ID for API post", STATUS_COLORS.error); + setDynamicStatus("Missing session ID", STATUS_COLORS.error); state.disconnectReason = "session_id_error"; // Track disconnect reason // Disconnect after a brief delay to ensure user sees the error message setTimeout(() => { @@ -1383,8 +1383,8 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { // Check if slot has been revoked if (data.allowed === false) { - debugWarn("[API QUEUE] MeshMapper API returned allowed=false, WarDriving slot has been revoked, disconnecting"); - setDynamicStatus("Error: Posting to API (Revoked)", STATUS_COLORS.error); + debugError("[API QUEUE] MeshMapper slot has been revoked"); + setDynamicStatus("API post failed (revoked)", STATUS_COLORS.error); state.disconnectReason = "slot_revoked"; // Track disconnect reason // Disconnect after a brief delay to ensure user sees the error message setTimeout(() => { @@ -1392,17 +1392,17 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { }, 1500); return; // Exit early after slot revocation } else if (data.allowed === true) { - debugLog("[API QUEUE] 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 MeshMapper slot"); } else { - debugWarn(`[API QUEUE] MeshMapper API response missing 'allowed' field: ${JSON.stringify(data)}`); + debugError(`[API QUEUE] MeshMapper API response missing 'allowed' field: ${JSON.stringify(data)}`); } } catch (parseError) { - debugWarn(`[API QUEUE] Failed to parse MeshMapper API response: ${parseError.message}`); + debugError(`[API QUEUE] Failed to parse MeshMapper API response: ${parseError.message}`); // Continue operation if we can't parse the response } if (!response.ok) { - debugWarn(`[API QUEUE] MeshMapper API returned error status ${response.status}`); + debugError(`[API QUEUE] MeshMapper API returned error status ${response.status}`); } else { debugLog(`[API QUEUE] MeshMapper API post successful (status ${response.status})`); } @@ -1656,7 +1656,7 @@ async function flushApiQueue() { // Validate session_id exists if (!state.wardriveSessionId) { debugError("[API QUEUE] Cannot flush: no session_id available"); - setDynamicStatus("Error: No session ID for API post", STATUS_COLORS.error); + setDynamicStatus("Missing session ID", STATUS_COLORS.error); state.disconnectReason = "session_id_error"; setTimeout(() => { disconnect().catch(err => debugError(`[BLE] Disconnect after missing session_id failed: ${err.message}`)); @@ -1681,8 +1681,8 @@ async function flushApiQueue() { // Check if slot has been revoked if (data.allowed === false) { - debugWarn("[API QUEUE] Slot has been revoked by API"); - setDynamicStatus("Error: Posting to API (Revoked)", STATUS_COLORS.error); + debugError("[API QUEUE] MeshMapper slot has been revoked"); + setDynamicStatus("API post failed (revoked)", STATUS_COLORS.error); state.disconnectReason = "slot_revoked"; setTimeout(() => { disconnect().catch(err => debugError(`[BLE] Disconnect after slot revocation failed: ${err.message}`)); @@ -1692,11 +1692,11 @@ async function flushApiQueue() { debugLog("[API QUEUE] Slot check passed"); } } catch (parseError) { - debugWarn(`[API QUEUE] Failed to parse response: ${parseError.message}`); + debugError(`[API QUEUE] Failed to parse response: ${parseError.message}`); } if (!response.ok) { - debugWarn(`[API QUEUE] API returned error status ${response.status}`); + debugError(`[API QUEUE] API returned error status ${response.status}`); setDynamicStatus("Error: API batch post failed", STATUS_COLORS.error); } else { debugLog(`[API QUEUE] Batch post successful: ${txCount} TX, ${rxCount} RX`); @@ -3892,23 +3892,23 @@ async function connect() { debugLog(`[BLE] Setting terminal status for reason: ${state.disconnectReason}`); } else if (state.disconnectReason === "capacity_full") { debugLog("[BLE] Branch: capacity_full"); - setDynamicStatus("WarDriving app has reached capacity", STATUS_COLORS.error, true); + setDynamicStatus("MeshMapper at capacity", STATUS_COLORS.error, true); debugLog("[BLE] Setting terminal status for capacity full"); } else if (state.disconnectReason === "app_down") { debugLog("[BLE] Branch: app_down"); - setDynamicStatus("WarDriving app is down", STATUS_COLORS.error, true); + setDynamicStatus("MeshMapper unavailable", STATUS_COLORS.error, true); debugLog("[BLE] Setting terminal status for app down"); } else if (state.disconnectReason === "slot_revoked") { debugLog("[BLE] Branch: slot_revoked"); - setDynamicStatus("WarDriving slot has been revoked", STATUS_COLORS.error, true); + setDynamicStatus("MeshMapper slot revoked", STATUS_COLORS.error, true); debugLog("[BLE] Setting terminal status for slot revocation"); } else if (state.disconnectReason === "session_id_error") { debugLog("[BLE] Branch: session_id_error"); - setDynamicStatus("Session ID error; try reconnecting", STATUS_COLORS.error, true); + setDynamicStatus("Session error - reconnect", STATUS_COLORS.error, true); debugLog("[BLE] Setting terminal status for session_id error"); } else if (state.disconnectReason === "public_key_error") { debugLog("[BLE] Branch: public_key_error"); - setDynamicStatus("Unable to read device public key; try again", STATUS_COLORS.error, true); + setDynamicStatus("Device key error - reconnect", STATUS_COLORS.error, true); debugLog("[BLE] Setting terminal status for public key error"); } else if (state.disconnectReason === "channel_setup_error") { debugLog("[BLE] Branch: channel_setup_error"); diff --git a/docs/CONNECTION_WORKFLOW.md b/docs/CONNECTION_WORKFLOW.md index fe9a689..3155ad0 100644 --- a/docs/CONNECTION_WORKFLOW.md +++ b/docs/CONNECTION_WORKFLOW.md @@ -155,12 +155,12 @@ connectBtn.addEventListener("click", async () => { - Shows fallback message: "Connection not allowed: [reason]" - If no reason code provided (backward compatibility): - Sets `state.disconnectReason = "capacity_full"` - - **Dynamic Status**: `"WarDriving app has reached capacity"` (red, terminal) + - **Dynamic Status**: `"MeshMapper at capacity"` (red, terminal) - If API error: - Sets `state.disconnectReason = "app_down"` - Triggers disconnect sequence after 1.5s delay (fail-closed) - **Connection Status**: `"Connecting"` → `"Disconnecting"` → `"Disconnected"` (red) - - **Dynamic Status**: `"Acquiring wardriving slot"` → `"WarDriving app is down"` (red, terminal) + - **Dynamic Status**: `"Acquiring wardriving slot"` → `"MeshMapper unavailable"` (red, terminal) - On success: - **Connection Status**: `"Connecting"` (blue, maintained) - **Dynamic Status**: `"Acquired wardriving slot"` (green) @@ -288,10 +288,11 @@ See `content/wardrive.js` for the main `disconnect()` function. - **Connection Status**: `"Disconnected"` (red) - ALWAYS set regardless of reason - **Dynamic Status**: Set based on `state.disconnectReason` (WITHOUT "Disconnected:" prefix): - API reason codes in `REASON_MESSAGES` (e.g., `outofdate` → `"App out of date, please update"`) (red) - - `capacity_full` → `"WarDriving app has reached capacity"` (red) - - `app_down` → `"WarDriving app is down"` (red) - - `slot_revoked` → `"WarDriving slot has been revoked"` (red) - - `public_key_error` → `"Unable to read device public key; try again"` (red) + - `capacity_full` → `"MeshMapper at capacity"` (red) + - `app_down` → `"MeshMapper unavailable"` (red) + - `slot_revoked` → `"MeshMapper slot revoked"` (red) + - `public_key_error` → `"Device key error - reconnect"` (red) + - `session_id_error` → `"Session error - reconnect"` (red) - `channel_setup_error` → Error message (red) - `ble_disconnect_error` → Error message (red) - `normal` / `null` / `undefined` → `"—"` (em dash) @@ -361,25 +362,25 @@ When a wardriving slot is revoked during an active session (detected during API 4. **Terminal Status** - Disconnect event handler detects `slot_revoked` reason - **Connection Status**: `"Disconnected"` (red) - - **Dynamic Status**: `"WarDriving slot has been revoked"` (red, terminal - NO "Disconnected:" prefix) + - **Dynamic Status**: `"MeshMapper slot revoked"` (red, terminal - NO "Disconnected:" prefix) - This is the final terminal status **Complete Revocation Flow (Updated for background API posting):** ``` Connection Status: (unchanged) → "Disconnecting" → "Disconnected" -Dynamic Status: "Idle"/"Waiting for next ping" → "Error: Posting to API (Revoked)" → "—" → "WarDriving slot has been revoked" +Dynamic Status: "Idle"/"Waiting for next ping" → "API post failed (revoked)" → "—" → "MeshMapper slot revoked" ``` **Timeline:** - T+0s: RX window completes, status shows "Idle" or "Waiting for next ping", next timer starts - T+0-3s: Background API post running (3s delay, then POST) - silent -- T+3-4s: Revocation detected, "Error: Posting to API (Revoked)" shown (1.5s) +- T+3-4s: Revocation detected, "API post failed (revoked)" shown (1.5s) - T+4.5s: Disconnect initiated -- T+5s: Terminal status "WarDriving slot has been revoked" +- T+5s: Terminal status "MeshMapper slot revoked" **Key Differences from Normal Disconnect:** - Normal disconnect: Dynamic Status shows `"—"` (em dash) -- Revocation: Dynamic Status shows `"WarDriving slot has been revoked"` (red error, no prefix) -- Revocation shows intermediate "Error: Posting to API (Revoked)" state +- Revocation: Dynamic Status shows `"MeshMapper slot revoked"` (red error, no prefix) +- Revocation shows intermediate "API post failed (revoked)" state - With the new ping/repeat flow, revocation may be detected after user already sees "Idle" or "Waiting for next ping" (because API runs in background) ## Workflow Diagrams diff --git a/docs/STATUS_MESSAGES.md b/docs/STATUS_MESSAGES.md index fde7ad0..c853f0a 100644 --- a/docs/STATUS_MESSAGES.md +++ b/docs/STATUS_MESSAGES.md @@ -110,55 +110,55 @@ These messages appear in the Dynamic App Status Bar. They NEVER include connecti - **When**: Capacity check passed successfully, slot acquired from MeshMapper API - **Source**: `content/wardrive.js:connect()` -##### WarDriving app has reached capacity -- **Message**: `"WarDriving app has reached capacity"` +##### MeshMapper at capacity +- **Message**: `"MeshMapper at capacity"` - **Color**: Red (error) - **When**: Capacity check API denies slot on connect (returns allowed=false) - **Terminal State**: Yes (persists until user takes action) -- **Notes**: Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "WarDriving app has reached capacity" (terminal) +- **Notes**: Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "MeshMapper at capacity" (terminal) -##### WarDriving app is down -- **Message**: `"WarDriving app is down"` +##### MeshMapper unavailable +- **Message**: `"MeshMapper unavailable"` - **Color**: Red (error) - **When**: Capacity check API returns error status or network is unreachable during connect - **Terminal State**: Yes (persists until user takes action) -- **Notes**: Implements fail-closed policy - connection denied if API fails. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "WarDriving app is down" (terminal) +- **Notes**: Implements fail-closed policy - connection denied if API fails. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "MeshMapper unavailable" (terminal) -##### WarDriving slot has been revoked -- **Message**: `"WarDriving slot has been revoked"` +##### MeshMapper slot revoked +- **Message**: `"MeshMapper slot revoked"` - **Color**: Red (error) - **When**: During active session, API returns allowed=false during background ping posting - **Terminal State**: Yes (persists until user takes action) - **Sequence** (Updated for background API posting): 1. RX listening window completes → Status shows "Idle" or "Waiting for next ping" 2. Background API post detects revocation (silent, no status change yet) - 3. "Error: Posting to API (Revoked)" (red, 1.5s) + 3. "API post failed (revoked)" (red, 1.5s) 4. Connection bar: "Disconnecting" → "Disconnected" - 5. Dynamic bar: "WarDriving slot has been revoked" (terminal) + 5. Dynamic bar: "MeshMapper slot revoked" (terminal) - **Notes**: With the new ping/repeat flow, revocation is detected during the background API post (which runs after the RX window completes and next timer starts) -##### Error: Posting to API (Revoked) -- **Message**: `"Error: Posting to API (Revoked)"` +##### API post failed (revoked) +- **Message**: `"API post failed (revoked)"` - **Color**: Red (error) - **When**: Intermediate status shown when slot revocation detected during background API posting - **Duration**: 1.5 seconds (visible before disconnect begins) - **Notes**: First visible status in revocation sequence, followed by disconnect flow. Appears after background API post detects revocation. -##### Unable to read device public key; try again -- **Message**: `"Unable to read device public key; try again"` +##### Device key error - reconnect +- **Message**: `"Device key error - reconnect"` - **Color**: Red (error) - **When**: Device public key is missing or invalid during connection - **Terminal State**: Yes - **Notes**: Triggers automatic disconnect -##### Session ID error; try reconnecting -- **Message**: `"Session ID error; try reconnecting"` +##### Session error - reconnect +- **Message**: `"Session error - reconnect"` - **Color**: Red (error) - **When**: - Capacity check returns allowed=true but session_id is missing during connection - Attempting to post to MeshMapper API without a valid session_id - **Terminal State**: Yes (persists until user takes action) -- **Notes**: Implements fail-closed policy - connection/posting denied if session_id is missing. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "Session ID error; try reconnecting" (terminal) +- **Notes**: Implements fail-closed policy - connection/posting denied if session_id is missing. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "Session error - reconnect" (terminal) - **Source**: `content/wardrive.js:checkCapacity()`, `content/wardrive.js:postToMeshMapperAPI()` ##### App out of date, please update @@ -177,8 +177,8 @@ These messages appear in the Dynamic App Status Bar. They NEVER include connecti - **Notes**: Fallback message for future/unknown reason codes. Shows the raw reason code to help with debugging. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "Connection not allowed: [reason]" (terminal) - **Source**: `content/wardrive.js` disconnected event handler -##### Error: No session ID for API post -- **Message**: `"Error: No session ID for API post"` +##### Missing session ID +- **Message**: `"Missing session ID"` - **Color**: Red (error) - **When**: Intermediate status shown when attempting to post to MeshMapper API without a valid session_id - **Duration**: 1.5 seconds (visible before disconnect begins) From da8f6dab001ef9de891f0ee181c35ac56b6b02f3 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Tue, 23 Dec 2025 07:43:39 -0500 Subject: [PATCH 77/77] Bump version to 1.6.0 in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fa550a..e5a970d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MeshCore GOME WarDriver -[![Version](https://img.shields.io/badge/version-1.5.0-blue.svg)](https://github.com/MrAlders0n/MeshCore-GOME-WarDriver/releases/tag/v1.5.0) +[![Version](https://img.shields.io/badge/version-1.6.0-blue.svg)](https://github.com/MrAlders0n/MeshCore-GOME-WarDriver/releases/tag/v1.6.0) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Android%20%7C%20iOS-orange.svg)](#platform-support)