diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index de98397..5a6a54b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,16 +48,19 @@ jobs: mkdir -p _site cp -r main-content/* _site/ 2>/dev/null || true - # Inject version into main branch + # Inject version into main branch (visual display and JS constant) sed -i 's|MeshCore Wardrive|MeshCore Wardrive '"${RELEASE_VERSION}"'|' _site/index.html + sed -i 's|const APP_VERSION = "UNKNOWN";|const APP_VERSION = "'"${RELEASE_VERSION}"'";|' _site/content/wardrive.js mkdir -p _site/dev cp -r dev-content/* _site/dev/ 2>/dev/null || true cp -r dev-content/content _site/dev/ 2>/dev/null || true - # Inject dev badge with date + # Inject dev badge with date (visual display and JS constant with DEV-EPOCH format) DEV_DATE=$(date -u +"%Y-%m-%d %H:%M UTC") + DEV_EPOCH=$(date -u +%s) sed -i 's|MeshCore Wardrive|MeshCore Wardrive DEV '"${DEV_DATE}"'|' _site/dev/index.html + sed -i 's|const APP_VERSION = "UNKNOWN";|const APP_VERSION = "DEV-'"${DEV_EPOCH}"'";|' _site/dev/content/wardrive.js find _site -name ". git" -exec rm -rf {} + 2>/dev/null || true find _site -name ".github" -exec rm -rf {} + 2>/dev/null || true diff --git a/content/wardrive.js b/content/wardrive.js index e56379c..ecbbcf1 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -5,7 +5,7 @@ // - Manual "Send Ping" and Auto mode (interval selectable: 15/30/60s) // - Acquire wake lock during auto mode to keep screen awake -import { WebBleConnection, Constants, Packet } from "./mc/index.js"; // your BLE client +import { WebBleConnection, Constants, Packet, BufferUtils } from "./mc/index.js"; // your BLE client // ---- Debug Configuration ---- // Enable debug logging via URL parameter (?debug=true) or set default here @@ -75,9 +75,16 @@ const MIN_PING_DISTANCE_M = 25; // Minimum distance (25m) between pings // MeshMapper API Configuration const MESHMAPPER_API_URL = "https://yow.meshmapper.net/wardriving-api.php"; +const MESHMAPPER_CAPACITY_CHECK_URL = "https://yow.meshmapper.net/capacitycheck.php"; const MESHMAPPER_API_KEY = "59C7754DABDF5C11CA5F5D8368F89"; const MESHMAPPER_DEFAULT_WHO = "GOME-WarDriver"; // Default identifier +// ---- App Version Configuration ---- +// This constant is injected by GitHub Actions during build/deploy +// For release builds: Contains the release version (e.g., "v1.3.0") +// For DEV builds: Contains "DEV-" format (e.g., "DEV-1734652800") +const APP_VERSION = "UNKNOWN"; // Placeholder - replaced during build + // ---- DOM refs (from index.html; unchanged except the two new selectors) ---- const $ = (id) => document.getElementById(id); const statusEl = $("status"); @@ -121,6 +128,10 @@ const state = { lastSuccessfulPingLocation: null, // { lat, lon } of the last successful ping (Mesh + API) distanceUpdateTimer: null, // Timer for updating distance display 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) + disconnectReason: null, // Tracks the reason for disconnection (e.g., "app_down", "capacity_full", "public_key_error", "channel_setup_error", "ble_disconnect_error", "normal") + channelSetupErrorMessage: null, // Error message from channel setup failure + bleDisconnectErrorMessage: null, // Error message from BLE disconnect failure repeaterTracking: { isListening: false, // Whether we're currently listening for echoes sentTimestamp: null, // Timestamp when the ping was sent @@ -416,8 +427,19 @@ function startCooldown() { function updateControlsForCooldown() { const connected = !!state.connection; const inCooldown = isInCooldown(); - sendPingBtn.disabled = !connected || inCooldown; - autoToggleBtn.disabled = !connected || inCooldown; + debugLog(`updateControlsForCooldown: connected=${connected}, inCooldown=${inCooldown}, pingInProgress=${state.pingInProgress}`); + sendPingBtn.disabled = !connected || inCooldown || state.pingInProgress; + autoToggleBtn.disabled = !connected || inCooldown || state.pingInProgress; +} + +/** + * Helper function to unlock ping controls after ping operation completes + * @param {string} reason - Debug reason for unlocking controls + */ +function unlockPingControls(reason) { + state.pingInProgress = false; + updateControlsForCooldown(); + debugLog(`Ping controls unlocked (pingInProgress=false) ${reason}`); } // Timer cleanup @@ -451,6 +473,12 @@ function cleanupAllTimers() { // Clear captured ping coordinates state.capturedPingCoords = null; + + // Clear ping in progress flag + state.pingInProgress = false; + + // Clear device public key + state.devicePublicKey = null; } function enableControls(connected) { @@ -939,13 +967,16 @@ async function ensureChannel() { return state.channel; } + setStatus("Looking for #wardriving channel", STATUS_COLORS.info); debugLog(`Looking up channel: ${CHANNEL_NAME}`); let ch = await state.connection.findChannelByName(CHANNEL_NAME); if (!ch) { + setStatus("Channel #wardriving not found", STATUS_COLORS.info); debugLog(`Channel ${CHANNEL_NAME} not found, attempting to create it`); try { ch = await createWardriveChannel(); + setStatus("Created #wardriving", STATUS_COLORS.success); debugLog(`Channel ${CHANNEL_NAME} created successfully`); } catch (e) { debugError(`Failed to create channel ${CHANNEL_NAME}: ${e.message}`); @@ -955,6 +986,7 @@ async function ensureChannel() { ); } } else { + setStatus("Channel #wardriving found", STATUS_COLORS.success); debugLog(`Channel found: ${CHANNEL_NAME} (index: ${ch.channelIdx})`); } @@ -1004,6 +1036,74 @@ function getDeviceIdentifier() { return (deviceText && deviceText !== "—") ? deviceText : MESHMAPPER_DEFAULT_WHO; } +/** + * Check capacity / slot availability with MeshMapper API + * @param {string} reason - Either "connect" (acquire slot) or "disconnect" (release slot) + * @returns {Promise} True if allowed to continue, false otherwise + */ +async function checkCapacity(reason) { + // Validate public key exists + if (!state.devicePublicKey) { + debugError("checkCapacity called but no public key stored"); + return reason === "connect" ? false : true; // Fail closed on connect, allow disconnect + } + + // Set status for connect requests + if (reason === "connect") { + setStatus("Acquiring wardriving slot", STATUS_COLORS.info); + } + + try { + const payload = { + key: MESHMAPPER_API_KEY, + public_key: state.devicePublicKey, + who: getDeviceIdentifier(), + reason: reason + }; + + debugLog(`Checking capacity: reason=${reason}, public_key=${state.devicePublicKey.substring(0, 16)}..., who=${payload.who}`); + + const response = await fetch(MESHMAPPER_CAPACITY_CHECK_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + debugWarn(`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"); + state.disconnectReason = "app_down"; // Track disconnect reason + return false; + } + return true; // Always allow disconnect to proceed + } + + const data = await response.json(); + debugLog(`Capacity check response: allowed=${data.allowed}`); + + // Handle capacity full vs. allowed cases separately + if (data.allowed === false && reason === "connect") { + state.disconnectReason = "capacity_full"; // Track disconnect reason + } + + return data.allowed === true; + + } catch (error) { + debugError(`Capacity check failed: ${error.message}`); + + // Fail closed on network errors for connect + if (reason === "connect") { + debugError("Failing closed (denying connection) due to network error"); + state.disconnectReason = "app_down"; // Track disconnect reason + return false; + } + + return true; // Always allow disconnect to proceed + } +} + /** * Post wardrive ping data to MeshMapper API * @param {number} lat - Latitude @@ -1019,10 +1119,11 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { who: getDeviceIdentifier(), power: getCurrentPowerSetting() || "N/A", heard_repeats: heardRepeats, + ver: APP_VERSION, test: 0 }; - debugLog(`Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}, heard_repeats=${heardRepeats}`); + 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}`); const response = await fetch(MESHMAPPER_API_URL, { method: "POST", @@ -1030,6 +1131,34 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { body: JSON.stringify(payload) }); + debugLog(`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)}`); + + // Check if slot has been revoked + if (data.allowed === false) { + debugWarn("MeshMapper API returned allowed=false, WarDriving slot has been revoked, disconnecting"); + setStatus("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}`)); + }, 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"); + } else { + debugWarn(`MeshMapper API response missing 'allowed' field: ${JSON.stringify(data)}`); + } + } catch (parseError) { + debugWarn(`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}`); } else { @@ -1074,6 +1203,9 @@ async function postApiAndRefreshMap(lat, lon, accuracy, heardRepeats) { debugLog(`Skipping map refresh (accuracy ${accuracy}m exceeds threshold)`); } + // Unlock ping controls now that API post is complete + unlockPingControls("after API post completion"); + // Update status based on current mode if (state.connection) { if (state.running) { @@ -1718,6 +1850,11 @@ async function sendPing(manual = false) { // Both validations passed - execute ping operation (Mesh + API) debugLog("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)"); + const payload = buildPayload(lat, lon); debugLog(`Sending ping to channel: "${payload}"`); @@ -1788,6 +1925,9 @@ async function sendPing(manual = false) { // 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`); + + // Unlock ping controls since API post is being skipped + unlockPingControls("after skipping API post due to missing coordinates"); } // Clear captured coordinates after API post completes (always, regardless of path) @@ -1803,6 +1943,9 @@ async function sendPing(manual = false) { } catch (e) { debugError(`Ping operation failed: ${e.message}`, e); setStatus(e.message || "Ping failed", STATUS_COLORS.error); + + // Unlock ping controls on error + unlockPingControls("after error"); } } @@ -1923,11 +2066,29 @@ async function connect() { conn.on("connected", async () => { debugLog("BLE connected event fired"); - setStatus("Connected", STATUS_COLORS.success); + // 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]"}`); + + // Validate and store public key + if (!selfInfo?.publicKey || selfInfo.publicKey.length !== 32) { + debugError("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}`)); + }, 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)}...`); + deviceInfoEl.textContent = selfInfo?.name || "[No device]"; updateAutoButton(); try { @@ -1937,21 +2098,92 @@ async function connect() { debugLog("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"); + // 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}`)); + }, 1500); + return; + } + + // Capacity check passed + setStatus("Acquired wardriving slot", STATUS_COLORS.success); + debugLog("Wardriving slot acquired successfully"); + + // Proceed with channel setup and GPS initialization await ensureChannel(); + + // GPS initialization + setStatus("Priming GPS", STATUS_COLORS.info); + debugLog("Starting GPS initialization"); await primeGpsOnce(); + + // Connection complete, show Connected status + setStatus("Connected", STATUS_COLORS.success); + debugLog("Full connection process completed successfully"); } catch (e) { debugError(`Channel setup failed: ${e.message}`, e); - setStatus(e.message || "Channel setup failed", STATUS_COLORS.error); + 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"); - setStatus("Disconnected", STATUS_COLORS.error); + debugLog(`Disconnect reason: ${state.disconnectReason}`); + + // Set appropriate status message based on disconnect reason + if (state.disconnectReason === "capacity_full") { + debugLog("Branch: capacity_full"); + setStatus("Disconnected: WarDriving app has reached capacity", STATUS_COLORS.error, true); + debugLog("Setting terminal status for capacity full"); + } else if (state.disconnectReason === "app_down") { + debugLog("Branch: app_down"); + setStatus("Disconnected: WarDriving app is down", STATUS_COLORS.error, true); + debugLog("Setting terminal status for app down"); + } else if (state.disconnectReason === "slot_revoked") { + debugLog("Branch: slot_revoked"); + setStatus("Disconnected: WarDriving slot has been revoked", STATUS_COLORS.error, true); + debugLog("Setting terminal status for slot revocation"); + } else if (state.disconnectReason === "public_key_error") { + debugLog("Branch: public_key_error"); + setStatus("Disconnected: Unable to read device public key", STATUS_COLORS.error, true); + debugLog("Setting terminal status for public key error"); + } else if (state.disconnectReason === "channel_setup_error") { + debugLog("Branch: channel_setup_error"); + const errorMsg = state.channelSetupErrorMessage || "Channel setup failed"; + setStatus(`Disconnected: ${errorMsg}`, STATUS_COLORS.error, true); + debugLog("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"); + const errorMsg = state.bleDisconnectErrorMessage || "BLE disconnect failed"; + setStatus(`Disconnected: ${errorMsg}`, STATUS_COLORS.error, true); + debugLog("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"); + setStatus("Disconnected", STATUS_COLORS.error, true); + } else { + debugLog(`Branch: else (unknown reason: ${state.disconnectReason})`); + // For unknown disconnect reasons, show generic disconnected message + debugLog(`Showing generic disconnected message for unknown reason: ${state.disconnectReason}`); + setStatus("Disconnected", STATUS_COLORS.error, true); + } + setConnectButton(false); deviceInfoEl.textContent = "—"; state.connection = null; state.channel = null; + state.devicePublicKey = null; // Clear public key + state.disconnectReason = null; // Reset disconnect reason + state.channelSetupErrorMessage = null; // Clear error message + state.bleDisconnectErrorMessage = null; // Clear error message stopAutoPing(true); // Ignore cooldown check on disconnect enableControls(false); updateAutoButton(); @@ -1985,8 +2217,25 @@ async function disconnect() { } connectBtn.disabled = true; + + // Set disconnectReason to "normal" if not already set (for user-initiated disconnects) + if (state.disconnectReason === null || state.disconnectReason === undefined) { + state.disconnectReason = "normal"; + } + setStatus("Disconnecting", STATUS_COLORS.info); + // Release capacity slot if we have a public key + if (state.devicePublicKey) { + try { + debugLog("Releasing capacity slot"); + await checkCapacity("disconnect"); + } catch (e) { + debugWarn(`Failed to release capacity slot: ${e.message}`); + // Don't fail disconnect if capacity release fails + } + } + // Delete the wardriving channel before disconnecting try { if (state.channel && typeof state.connection.deleteChannel === "function") { @@ -2015,7 +2264,8 @@ async function disconnect() { } } catch (e) { debugError(`BLE disconnect failed: ${e.message}`, e); - setStatus(e.message || "Disconnect failed", STATUS_COLORS.error); + state.disconnectReason = "ble_disconnect_error"; // Mark specific disconnect reason + state.bleDisconnectErrorMessage = e.message || "Disconnect failed"; // Store error message } finally { connectBtn.disabled = false; } diff --git a/docs/CONNECTION_WORKFLOW.md b/docs/CONNECTION_WORKFLOW.md new file mode 100644 index 0000000..5fb424d --- /dev/null +++ b/docs/CONNECTION_WORKFLOW.md @@ -0,0 +1,569 @@ +# Application Workflow Documentation + +## Table of Contents +- [Overview](#overview) + - [Connection Overview](#connection-overview) + - [Disconnection Overview](#disconnection-overview) +- [Connection Workflow](#connection-workflow) +- [Disconnection Workflow](#disconnection-workflow) +- [Workflow Diagrams](#workflow-diagrams) +- [Code References](#code-references) +- [Edge Cases and Gotchas](#edge-cases-and-gotchas) + +## Overview + +### Connection Overview + +**What "Connect" Means:** +- Establishes a Web Bluetooth (BLE) connection to a MeshCore companion device +- Configures the device for wardriving operations +- Acquires an API slot from the MeshMapper backend for capacity management +- Creates or finds the `#wardriving` channel for sending GPS pings +- Initializes GPS tracking for location services +- Enables the app to send wardrive pings to the mesh network + +**Connected State Enables:** +- Manual ping transmission (via "Send Ping" button) +- Automatic ping transmission (via "Start Auto Ping" with configurable intervals: 15s/30s/60s) +- GPS coordinate tracking and display +- Real-time repeater echo detection +- Integration with MeshMapper API for coverage mapping +- Session ping history logging + +### Disconnection Overview + +**What "Disconnect" Means:** +- Cleanly terminates the BLE connection to the MeshCore device +- Releases the API slot back to the MeshMapper backend +- Deletes the `#wardriving` channel from the device (cleanup) +- Stops all running timers and operations (auto-ping, GPS watch, wake locks) +- Clears all connection state and resets the UI +- Returns the application to idle state, ready for a new connection + +**Expected App State After Disconnect:** +- Status: "Disconnected" (red) +- All controls disabled except "Connect" button +- GPS tracking stopped +- Auto-ping mode disabled +- All timers cleared +- Connection state reset +- Ready to initiate a new connection + +## Connection Workflow + +### Connection Steps (High-Level) + +1. **User Initiates** → User clicks "Connect" button +2. **Device Selection** → Browser shows BLE device picker +3. **BLE GATT Connection** → Establishes GATT connection to device +4. **Protocol Handshake** → Exchanges protocol version +5. **Device Info** → Retrieves device name, public key, settings +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 + +### Detailed Connection Steps + +See `content/wardrive.js` lines 2020-2150 for the main `connect()` function. + +**Key Entry Point:** +```javascript +connectBtn.addEventListener("click", async () => { + if (state.connection) { + await disconnect(); + } else { + await connect(); + } +}); +``` + +**Connection Sequence:** + +1. **Validate Web Bluetooth Support** + - Checks `navigator.bluetooth` exists + - Alerts user if not supported + - Fails fast if unavailable + - **Status**: N/A (alert shown) + +2. **Open BLE Connection** + - Calls `WebBleConnection.open()` (web_ble_connection.js:15-41) + - Shows browser's native device picker + - Filters for MeshCore BLE service UUID + - User selects device or cancels + - **Status**: `"Connecting"` (blue) + +3. **Initialize BLE** + - Connects to GATT server + - Discovers service and characteristics (RX/TX) + - Starts notifications on TX characteristic + - Sets up frame listener for incoming data + - Fires "connected" event + - **Status**: `"Connecting"` (blue, maintained) + +4. **Device Query** + - Sends protocol version query + - Non-critical, errors ignored + - **Status**: `"Connecting"` (blue, maintained) + +5. **Get Device Info** + - Retrieves device name, public key (32 bytes), settings + - **CRITICAL**: Validates public key length + - Converts to hex string + - Stores in `state.devicePublicKey` + - Updates UI with device name + - Changes button to "Disconnect" (red) + - **Status**: `"Connecting"` (blue, maintained) + +6. **Sync Device Time** + - Sends current Unix timestamp + - Device updates its clock + - Optional, errors ignored + - **Status**: `"Connecting"` (blue, maintained) + +7. **Check Capacity** + - **Status**: `"Acquiring wardriving slot"` (blue) + - POSTs to MeshMapper API: + ```json + { + "key": "API_KEY", + "public_key": "device_hex_key", + "who": "device_name", + "reason": "connect" + } + ``` + - If `allowed: false`: + - Sets `state.disconnectReason = "capacity_full"` + - Triggers disconnect sequence after 1.5s delay + - **Status**: `"Disconnecting"` (blue) → `"Disconnected: WarDriving app has reached capacity"` (red) + - If API error: + - Sets `state.disconnectReason = "app_down"` + - Triggers disconnect sequence after 1.5s delay (fail-closed) + - **Status**: `"Disconnecting"` (blue) → `"Disconnected: WarDriving app is down"` (red) + - On success → **Status**: `"Acquired wardriving slot"` (green) + +8. **Setup Channel** + - **Status**: `"Looking for #wardriving channel"` (blue) + - Searches for existing `#wardriving` channel + - If found: + - **Status**: `"Channel #wardriving found"` (green) + - Stores channel object in `state.channel` + - Updates UI: "#wardriving (CH:X)" + - If not found: + - **Status**: `"Channel #wardriving not found"` (blue) + - Creates new channel: + - Finds empty channel slot + - Derives channel key: `SHA-256(#wardriving).slice(0, 16)` + - Sends setChannel command + - **Status**: `"Created #wardriving"` (green) + - Stores channel object in `state.channel` + - Updates UI: "#wardriving (CH:X)" + +9. **Initialize GPS** + - **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** + - **Status**: `"Connected"` (green) + - Enables all UI controls + - Ready for wardriving operations + +## Disconnection Workflow + +### Disconnection Steps (High-Level) + +1. **Disconnect Trigger** → User clicks "Disconnect" or error occurs +2. **Status Update** → Shows "Disconnecting" +3. **Capacity Release** → Returns API slot to MeshMapper +4. **Channel Deletion** → Removes #wardriving channel from device +5. **BLE Disconnect** → Closes GATT connection +6. **Cleanup** → Stops timers, GPS, wake locks +7. **State Reset** → Clears all connection state +8. **Disconnected** → Returns to idle state + +### Detailed Disconnection Steps + +See `content/wardrive.js` lines 2119-2179 for the main `disconnect()` function. + +**Disconnect Triggers:** +- User clicks "Disconnect" button +- Capacity denial during connect +- Public key validation failure +- Channel setup failure +- BLE connection lost (device out of range) + +**Disconnection Sequence:** + +1. **Disable Button** + - Prevents duplicate disconnect requests + +2. **Set Disconnect Reason** + - "normal" - user-initiated + - "capacity_full" - MeshMapper full + - "app_down" - API unavailable + - "error" - validation/setup failure + - "slot_revoked" - slot revoked during active session + +3. **Update Status** + - Sets status to "Disconnecting" (blue) + +4. **Release Capacity** + - POSTs to MeshMapper API with `reason: "disconnect"` + - **Fail-open**: errors ignored, always proceeds + +5. **Delete Channel** + - Sends `setChannel(idx, "", zeros)` to clear slot + - **Fail-open**: errors ignored, always proceeds + +6. **Close BLE** + - Tries `connection.close()` + - Falls back to `connection.disconnect()` + - Last resort: `device.gatt.disconnect()` + - Triggers "gattserverdisconnected" event + +7. **Disconnected Event Handler** + - Fires on BLE disconnect + - Runs comprehensive cleanup: + - Stops auto-ping mode + - Clears auto-ping timer + - Stops GPS watch + - Stops GPS age updater + - Stops distance updater + - Stops repeater tracking + - Clears all timers (see `cleanupAllTimers()`) + - Releases wake lock + - Clears connection state + - Clears device public key + +8. **UI Cleanup** + - Disables all controls except "Connect" + - Clears device info display + - Clears GPS display + - Clears distance display + - Changes button to "Connect" (green) + +9. **State Reset** + - `state.connection = null` + - `state.channel = null` + - `state.lastFix = null` + - `state.lastSuccessfulPingLocation = null` + - `state.gpsState = "idle"` + +10. **Disconnected Complete** + - Status: "Disconnected" (red) or error message + - All resources released + - Ready for new connection + +### Slot Revocation Workflow + +When a wardriving slot is revoked during an active session (detected during API posting), a special disconnect sequence occurs: + +**Revocation Detection:** +- Occurs during `postToMeshMapperAPI()` call (after every ping) +- API response contains `allowed: false` +- This indicates the backend has revoked the device's slot + +**Revocation Sequence:** + +1. **Detection** + - During "Posting to API" operation + - API returns `{"allowed": false, ...}` + - Detected in `postToMeshMapperAPI()` response handler + +2. **Initial Status** + - **Status**: `"Error: Posting to API (Revoked)"` (red) + - Sets `state.disconnectReason = "slot_revoked"` + - Visible for 1.5 seconds + +3. **Disconnect Initiated** + - Calls `disconnect()` after 1.5s delay + - **Status**: `"Disconnecting"` (blue) + - Proceeds with normal disconnect cleanup + +4. **Terminal Status** + - Disconnect event handler detects `slot_revoked` reason + - **Status**: `"Disconnected: WarDriving slot has been revoked"` (red) + - This is the final terminal status (does NOT revert to "Idle") + +**Complete Revocation Flow:** +``` +"Posting to API" (blue) + → "Error: Posting to API (Revoked)" (red, 1.5s) + → "Disconnecting" (blue) + → "Disconnected: WarDriving slot has been revoked" (red, terminal) +``` + +**Key Differences from Normal Disconnect:** +- Normal disconnect: ends with "Disconnected" (red) +- Revocation: ends with "Disconnected: WarDriving slot has been revoked" (red) +- Revocation shows intermediate "Error: Posting to API (Revoked)" state +- Terminal status is preserved and does not loop to "Idle" + +## Workflow Diagrams + +### Connection Sequence + +```mermaid +sequenceDiagram + actor User + participant UI + participant App as wardrive.js + participant BLE as WebBleConnection + participant Device as MeshCore Device + participant GPS + participant API as MeshMapper API + + User->>UI: Click "Connect" + UI->>App: connect() + App->>BLE: WebBleConnection.open() + BLE->>User: Show device picker + User->>BLE: Select device + BLE->>Device: GATT Connect + Device-->>BLE: Connected + BLE->>Device: Discover services + BLE->>App: Fire "connected" + + App->>Device: DeviceQuery + Device-->>App: DeviceInfo + + App->>Device: getSelfInfo() + Device-->>App: SelfInfo + public key + App->>App: Validate & store key + + App->>Device: syncDeviceTime() + Device-->>App: OK + + App->>API: checkCapacity("connect") + alt Allowed + API-->>App: {allowed: true} + App->>Device: findChannel("#wardriving") + alt Not found + App->>Device: createChannel() + end + App->>GPS: getCurrentPosition() + GPS-->>App: Position + App->>GPS: watchPosition() + App->>UI: Status "Connected" + else Denied + API-->>App: {allowed: false} + App->>App: disconnect() + end +``` + +### Disconnection Sequence + +```mermaid +sequenceDiagram + actor User + participant UI + participant App as wardrive.js + participant BLE as WebBleConnection + participant Device + participant GPS + participant API as MeshMapper API + + User->>UI: Click "Disconnect" + UI->>App: disconnect() + App->>UI: Status "Disconnecting" + + App->>API: checkCapacity("disconnect") + Note over API: Errors ignored + + App->>Device: deleteChannel() + Note over Device: Errors ignored + + App->>BLE: close() + BLE->>Device: GATT Disconnect + Device-->>BLE: Disconnected + BLE->>App: Fire "disconnected" + + App->>App: stopAutoPing() + App->>GPS: clearWatch() + App->>App: cleanupAllTimers() + App->>App: Reset state + + App->>UI: Status "Disconnected" + App->>UI: Disable controls + App->>UI: Button "Connect" +``` + +### State Machine + +```mermaid +stateDiagram-v2 + [*] --> Disconnected + + Disconnected --> Connecting: User clicks Connect + Connecting --> DeviceSelection: BLE available + Connecting --> Disconnected: BLE not supported + + DeviceSelection --> BLEConnection: Device selected + DeviceSelection --> Disconnected: User cancels + + BLEConnection --> DeviceInfo: GATT connected + BLEConnection --> Disconnected: GATT fails + + DeviceInfo --> CapacityCheck: Info valid + DeviceInfo --> Disconnecting: Invalid key + + CapacityCheck --> ChannelSetup: Slot acquired + CapacityCheck --> Disconnecting: Denied + + ChannelSetup --> GPSInit: Channel ready + ChannelSetup --> Disconnecting: Setup fails + + GPSInit --> Connected: GPS started + + Connected --> Disconnecting: User disconnect + Connected --> Disconnecting: BLE lost + + Disconnecting --> Disconnected: Cleanup complete +``` + +## Code References + +### Connect Entry Points +- **Main function**: `wardrive.js:connect()` (lines 2002-2118) +- **Button listener**: `wardrive.js` line 2207 +- **WebBLE open**: `web_ble_connection.js:WebBleConnection.open()` (lines 15-41) +- **BLE init**: `web_ble_connection.js:init()` (lines 43-77) + +### Disconnect Entry Points +- **Main function**: `wardrive.js:disconnect()` (lines 2119-2179) +- **Button listener**: `wardrive.js` line 2207 (same button, checks state) +- **Auto disconnect**: triggered by capacity/validation failures +- **BLE event**: `gattserverdisconnected` (web_ble_connection.js:46) + +### Key Connection Functions +- **Channel setup**: `wardrive.js:ensureChannel()` (lines 941-971) +- **Channel creation**: `wardrive.js:createWardriveChannel()` (lines 899-939) +- **Key derivation**: `wardrive.js:deriveChannelKey()` (lines 847-896) +- **GPS init**: `wardrive.js:primeGpsOnce()` (lines 804-842) +- **GPS watch**: `wardrive.js:startGeoWatch()` (lines 751-792) +- **Capacity check**: `wardrive.js:checkCapacity()` (lines 1018-1082) + +### Key Disconnection Functions +- **Timer cleanup**: `wardrive.js:cleanupAllTimers()` (lines 427-460) +- **Auto-ping stop**: `wardrive.js:stopAutoPing()` (lines 1904-1934) +- **GPS watch stop**: `wardrive.js:stopGeoWatch()` (lines 793-803) +- **Repeater stop**: `wardrive.js:stopRepeaterTracking()` (lines 1506-1544) +- **Channel delete**: `connection.js:deleteChannel()` (lines 1909-1911) + +### State Management +- **Global state**: `wardrive.js` lines 102-136 (`const state = {...}`) +- **Status management**: `wardrive.js:setStatus()` (lines 165-225) +- **Button state**: `wardrive.js:setConnectButton()` (lines 495-518) +- **Control state**: `wardrive.js:enableControls()` (lines 462-466) + +### Transport Implementation +- **WebBLE class**: `web_ble_connection.js` (lines 4-106) +- **Base connection**: `connection.js` (lines 9-2218) +- **Event emitter**: `events.js` +- **Constants**: `constants.js` + +## Edge Cases and Gotchas + +### Connect While Connected +- Button acts as toggle +- If connected, triggers disconnect +- No duplicate connections possible + +### Disconnect While Connecting +- Button disabled during connect +- Only auto-disconnects possible (capacity/validation) +- GATT disconnect triggers normal cleanup + +### Browser Refresh +- All state lost +- BLE connection dropped +- Session ping log cleared +- User must reconnect +- No auto-reconnect + +### Network Loss +- **BLE loss**: auto-disconnect via `gattserverdisconnected` +- **API timeout (connect)**: fail-closed, deny connection +- **API timeout (disconnect)**: fail-open, allow disconnect +- **Ping API timeout**: fail-open, ping considered sent + +### Reconnect Behavior +- No automatic reconnect +- User must manually click "Connect" +- Full connection sequence runs +- New capacity slot acquired +- Previous session data lost + +### Resource Leaks +- Comprehensive cleanup on disconnect +- All timers tracked and cleared +- All event listeners removed +- GPS watch cleared +- Wake lock released +- **No known leaks** ✅ + +### Capacity Management +- **Slot exhaustion**: connect denied, user waits +- **Release failure**: disconnect proceeds anyway +- **API downtime**: connect fails (fail-closed) +- **Orphaned slots**: rely on server-side timeout + +### Channel Management +- **No empty slots**: error shown, pings unavailable +- **Channel exists**: reuses existing, no warning +- **Delete failure**: logged, disconnect proceeds +- **Conflict risk**: ensure MeshCore app disconnected first + +### GPS Edge Cases +- **Permission denied**: error shown, pings still work +- **Low accuracy**: allowed, map refresh skipped +- **Timeout**: watch continues, may skip auto-pings +- **Signal loss**: watch polls, old data check enforced + +### Auto-Ping Interactions +- **Manual during auto**: auto pauses, resumes after +- **7s cooldown**: prevents rapid-fire pings +- **Control locking**: "Send Ping" and "Start Auto Ping" buttons remain locked for entire ping lifecycle: + - Locked when: ping sent → listening for repeats (7s) → finalizing repeats → posting to API (3s + API time) + - Unlocked when: API post completes or error occurs + - Prevents starting new pings while previous ping is still processing +- **Page hidden**: auto stops, must restart manually +- **Cooldown bypass**: only on disconnect + +### Error Recovery +- **Connect fails**: button re-enabled, user retries +- **Ping fails**: auto continues, no retry +- **API fails (connect)**: fail-closed, user retries +- **API fails (disconnect)**: fail-open, always succeeds + +### State Consistency +- Single state object (`state`) +- No partial states +- Clear transitions +- Error → Disconnected +- Recovery always possible + +## Summary + +MeshCore-GOME-WarDriver implements a robust Web Bluetooth wardriving application with clear connection/disconnection workflows: + +**Key Design Principles:** +1. **Fail-Closed on Connect**: API errors deny connection +2. **Fail-Open on Disconnect**: Always proceed regardless of errors +3. **Comprehensive Cleanup**: All resources explicitly released +4. **Clear State Machine**: No ambiguous states +5. **User Transparency**: Status messages at every step + +**Connection:** BLE → Device Info → Time Sync → Capacity Check → Channel Setup → GPS → Connected + +**Disconnection:** Capacity Release → Channel Delete → BLE Close → Full Cleanup → Disconnected + +**Debug Mode:** Add `?debug=true` to URL for detailed logging + +The workflow prioritizes reliability, clear error messages, and complete resource cleanup on every disconnect. diff --git a/docs/DEVELOPMENT_REQUIREMENTS.md b/docs/DEVELOPMENT_REQUIREMENTS.md new file mode 100644 index 0000000..cf53504 --- /dev/null +++ b/docs/DEVELOPMENT_REQUIREMENTS.md @@ -0,0 +1,76 @@ +# MeshCore GOME WarDriver - Development Guidelines + +## Overview +This document defines the coding standards and requirements for all changes to the MeshCore GOME WarDriver repository. AI agents and contributors must follow these guidelines for every modification. + +--- + +## Code Style & Standards + +### Debug Logging +- **ALWAYS** include debug console logging for significant operations +- Use the existing debug helper functions: + - `debugLog(message, ...args)` - For general debug information + - `debugWarn(message, ... args)` - For warning conditions + - `debugError(message, ... args)` - For error conditions +- 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 + +### 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 +- Use appropriate `STATUS_COLORS` constants: + - `STATUS_COLORS.idle` - Default/waiting state + - `STATUS_COLORS. success` - Successful operations + - `STATUS_COLORS.warning` - Warning conditions + - `STATUS_COLORS.error` - Error states + - `STATUS_COLORS.info` - Informational/in-progress states + +--- + +## Documentation Requirements + +### Code Comments +- Document complex logic with inline comments +- Use JSDoc-style comments for functions: + - `@param` for parameters + - `@returns` for return values + - Brief description of purpose + +### docs/STATUS_MESSAGES.md Updates +When adding new status messages, include: +- The exact status message text +- When it appears (trigger condition) +- The status color used +- Any follow-up actions or states + +### `docs/CONNECTION_WORKFLOW.md` Updates +When **modifying connect or disconnect logic**, you must: +- Read `docs/CONNECTION_WORKFLOW.md` before making the change (to understand current intended behavior). +- Update `docs/CONNECTION_WORKFLOW.md` so it remains accurate after the change: + - Steps/sequence of the workflow + - Any new states, retries, timeouts, or error handling + - Any UI impacts (buttons, indicators, status messages) + +--- +## Requested Change: Update App Connection Flow (Reorder Steps) + +### Background +Below is the **current** app connection flow used when a user connects to a device for wardriving. + +#### Current Connection Flow +1. **User Initiates** → User clicks **Connect** +2. **Device Selection** → Browser displays BLE device picker +3. **BLE GATT Connection** → App establishes a GATT connection to the selected device +4. **Protocol Handshake** → App and device exchange/confirm protocol version compatibility +5. **Device Info** → App retrieves device metadata (e.g., device name, public key, settings) +6. **Time Sync** → App synchronizes the device clock +7. **Channel Setup** → App creates or finds the `#wardriving` channel +8. **GPS Init** → App starts GPS tracking +9. **Capacity Check** → App acquires an API slot from **MeshMapper** +10. **Connected** → App enables all controls; system is ready for wardriving + +--- + +### Requested Change + diff --git a/STATUS_MESSAGES.md b/docs/STATUS_MESSAGES.md similarity index 56% rename from STATUS_MESSAGES.md rename to docs/STATUS_MESSAGES.md index 58f5e6a..3fe6075 100644 --- a/STATUS_MESSAGES.md +++ b/docs/STATUS_MESSAGES.md @@ -25,39 +25,41 @@ Status messages follow these consistent conventions: - **Message**: `"Connecting"` - **Color**: Sky blue (info) - **Used in**: `connect()` -- **Source**: `content/wardrive.js:1916` -- **Context**: When user clicks Connect button -- **Minimum Visibility**: Natural async timing during BLE pairing (typically 2-5 seconds) +- **Source**: `content/wardrive.js:2021` +- **Context**: When user clicks Connect button; remains visible during entire connection process (BLE pairing, capacity check, and channel setup) +- **Minimum Visibility**: Natural async timing during full connection process (typically 3-8 seconds including capacity check) #### Connected - **Message**: `"Connected"` - **Color**: Green (success) - **Used in**: `connect()` -- **Source**: `content/wardrive.js:1926` -- **Context**: After BLE device successfully pairs -- **Minimum Visibility**: 500ms minimum enforced +- **Source**: `content/wardrive.js:2080` +- **Context**: After full connection process completes successfully (BLE paired, capacity check passed, channel setup, and GPS initialized) +- **Minimum Visibility**: Persists until user interacts with app buttons (send ping, start auto mode) +- **Note**: This message now only appears after the complete connection handshake, not just after BLE pairing #### Disconnecting - **Message**: `"Disconnecting"` - **Color**: Sky blue (info) - **Used in**: `disconnect()` -- **Source**: `content/wardrive.js:1988` -- **Context**: When user clicks Disconnect button +- **Source**: `content/wardrive.js:2118` +- **Context**: When user clicks Disconnect button or when automatic disconnect is triggered - **Minimum Visibility**: 500ms minimum enforced #### Disconnected - **Message**: `"Disconnected"` - **Color**: Red (error) - **Used in**: `connect()`, `disconnect()`, event handlers -- **Source**: `content/wardrive.js:1950`, `content/wardrive.js:2046` -- **Context**: Initial state and when BLE device disconnects +- **Source**: `content/wardrive.js:2073`, `content/wardrive.js:2177` +- **Context**: Initial state and when BLE device disconnects normally (user-initiated or device-initiated) - **Minimum Visibility**: N/A (persists until connection is established) +- **Note**: Only shown for normal disconnections; error disconnections (e.g., app down, capacity full) preserve their specific error message #### Connection failed - **Message**: `"Connection failed"` (or error message) - **Color**: Red (error) - **Used in**: `connect()`, event handlers -- **Source**: `content/wardrive.js:1976`, `content/wardrive.js:2059` +- **Source**: `content/wardrive.js:2096`, `content/wardrive.js:2190` - **Context**: BLE connection fails or connection button error - **Minimum Visibility**: N/A (error state persists) @@ -65,7 +67,7 @@ Status messages follow these consistent conventions: - **Message**: `"Channel setup failed"` (or error message) - **Color**: Red (error) - **Used in**: `connect()` -- **Source**: `content/wardrive.js:1944` +- **Source**: `content/wardrive.js:2063` - **Context**: Channel creation or lookup fails during connection - **Minimum Visibility**: N/A (error state persists) @@ -73,13 +75,135 @@ Status messages follow these consistent conventions: - **Message**: `"Disconnect failed"` (or error message) - **Color**: Red (error) - **Used in**: `disconnect()` -- **Source**: `content/wardrive.js:2018` +- **Source**: `content/wardrive.js:2149` - **Context**: Error during disconnect operation - **Minimum Visibility**: N/A (error state persists) --- -### 2. Ping Operation Messages +### 2. Capacity Check Messages + +#### Acquiring wardriving slot +- **Message**: `"Acquiring wardriving slot"` +- **Color**: Sky blue (info) +- **Used in**: `checkCapacity()` +- **Source**: `content/wardrive.js:1033` +- **Context**: When connecting to device, after time sync and before channel setup, checking if a wardriving slot is available +- **Minimum Visibility**: 500ms minimum enforced (or until API response received) + +#### Acquired wardriving slot +- **Message**: `"Acquired wardriving slot"` +- **Color**: Green (success) +- **Used in**: `connect()` +- **Source**: `content/wardrive.js:2087` +- **Context**: Capacity check passed successfully, slot acquired from MeshMapper API +- **Minimum Visibility**: 500ms minimum enforced +- **Notes**: This message appears after "Acquiring wardriving slot" when the API confirms slot availability. Fixes spelling from previous "Aquired" typo. + +#### Disconnected: WarDriving app has reached capacity +- **Message**: `"Disconnected: WarDriving app has reached capacity"` +- **Color**: Red (error) +- **Used in**: `connect()` (disconnected event handler) +- **Source**: `content/wardrive.js` (disconnected event handler) +- **Context**: Capacity check API denies slot on connect (returns allowed=false) +- **Minimum Visibility**: N/A (error state persists as terminal status) +- **Notes**: This is the final status message when capacity check fails during connection. The complete sequence is: "Connecting" → "Acquiring wardriving slot" → "Disconnecting" → "Disconnected: WarDriving app has reached capacity". Message format standardized with "Disconnected: " prefix to clearly indicate disconnect state. + +#### Error: Posting to API (Revoked) +- **Message**: `"Error: Posting to API (Revoked)"` +- **Color**: Red (error) +- **Used in**: `postToMeshMapperAPI()` +- **Source**: `content/wardrive.js:1128` +- **Context**: Intermediate status shown when WarDriving API returns allowed=false during an active session +- **Minimum Visibility**: 1500ms (enforced by setTimeout delay before disconnect) +- **Notes**: This is the first status message shown when slot revocation is detected during API posting. After the delay, the disconnect sequence begins with "Disconnecting", followed by the terminal status "Disconnected: WarDriving slot has been revoked". + +#### Disconnected: WarDriving slot has been revoked +- **Message**: `"Disconnected: WarDriving slot has been revoked"` +- **Color**: Red (error) +- **Used in**: `connect()` (disconnected event handler) +- **Source**: `content/wardrive.js:2123` +- **Context**: Terminal status shown when WarDriving slot is revoked during an active session +- **Minimum Visibility**: N/A (error state persists as terminal status) +- **Notes**: This is the final status message in the slot revocation flow. The complete sequence is: "Posting to API" → "Error: Posting to API (Revoked)" → "Disconnecting" → "Disconnected: WarDriving slot has been revoked". This message is set by the disconnect event handler when state.disconnectReason is "slot_revoked". Message format standardized with "Disconnected: " prefix to clearly indicate disconnect state. + +#### Disconnected: WarDriving app is down +- **Message**: `"Disconnected: WarDriving app is down"` +- **Color**: Red (error) +- **Used in**: `connect()` (disconnected event handler) +- **Source**: `content/wardrive.js` (disconnected event handler) +- **Context**: Capacity check API returns error status or network is unreachable during connect +- **Minimum Visibility**: N/A (error state persists as terminal status) +- **Notes**: Implements fail-closed policy - connection is denied if API fails or is unreachable. The complete sequence is: "Connecting" → "Acquiring wardriving slot" → "Disconnecting" → "Disconnected: WarDriving app is down". Message format standardized with "Disconnected: " prefix to clearly indicate disconnect state. + +#### Unable to read device public key; try again +- **Message**: `"Unable to read device public key; try again"` +- **Color**: Red (error) +- **Used in**: `connect()` +- **Source**: `content/wardrive.js:2048` +- **Context**: Device public key is missing or invalid when trying to acquire capacity slot +- **Minimum Visibility**: N/A (error state persists until disconnect) + +#### Network issue checking slot, proceeding anyway +- **Message**: `"Network issue checking slot, proceeding anyway"` (DEPRECATED - no longer used) +- **Color**: Amber (warning) +- **Used in**: N/A (removed) +- **Source**: Previously `content/wardrive.js:1051`, `content/wardrive.js:1070` +- **Context**: This message is no longer shown. Network issues now result in connection denial (fail-closed) +- **Notes**: Replaced by fail-closed policy - connection is now denied on network errors + +--- + +### 3. Channel Setup Messages + +#### Looking for #wardriving channel +- **Message**: `"Looking for #wardriving channel"` +- **Color**: Sky blue (info) +- **Used in**: `ensureChannel()` +- **Source**: `content/wardrive.js:954` +- **Context**: During connection setup, after capacity check, searching for existing #wardriving channel +- **Minimum Visibility**: 500ms minimum enforced + +#### Channel #wardriving found +- **Message**: `"Channel #wardriving found"` +- **Color**: Green (success) +- **Used in**: `ensureChannel()` +- **Source**: `content/wardrive.js:971` +- **Context**: Existing #wardriving channel found on device +- **Minimum Visibility**: 500ms minimum enforced + +#### Channel #wardriving not found +- **Message**: `"Channel #wardriving not found"` +- **Color**: Sky blue (info) +- **Used in**: `ensureChannel()` +- **Source**: `content/wardrive.js:958` +- **Context**: #wardriving channel does not exist, will attempt to create it +- **Minimum Visibility**: 500ms minimum enforced + +#### Created #wardriving +- **Message**: `"Created #wardriving"` +- **Color**: Green (success) +- **Used in**: `ensureChannel()` +- **Source**: `content/wardrive.js:962` +- **Context**: Successfully created new #wardriving channel on device +- **Minimum Visibility**: 500ms minimum enforced + +--- + +### 4. GPS Initialization Messages + +#### Priming GPS +- **Message**: `"Priming GPS"` +- **Color**: Sky blue (info) +- **Used in**: `connect()` +- **Source**: `content/wardrive.js:2101` +- **Context**: Starting GPS initialization during connection setup +- **Minimum Visibility**: 500ms minimum enforced (or until GPS initialization completes) +- **Notes**: This status is shown after channel setup and before the final "Connected" status. + +--- + +### 5. Ping Operation Messages #### Sending manual ping - **Message**: `"Sending manual ping"` @@ -140,13 +264,13 @@ Status messages follow these consistent conventions: --- -### 3. GPS Status Messages +### 6. GPS Status Messages #### Waiting for GPS fix - **Message**: `"Waiting for GPS fix"` - **Color**: Amber (warning) - **Used in**: `getGpsCoordinatesForPing()` -- **Source**: `content/wardrive.js:1503` +- **Source**: `content/wardrive.js:1614` - **Context**: Auto ping triggered but no GPS lock acquired yet - **Minimum Visibility**: 500ms minimum enforced @@ -154,13 +278,13 @@ Status messages follow these consistent conventions: - **Message**: `"GPS data too old, requesting fresh position"` - **Color**: Amber (warning) - **Used in**: `getGpsCoordinatesForPing()` -- **Source**: `content/wardrive.js:1514`, `content/wardrive.js:1567` +- **Source**: `content/wardrive.js:1625`, `content/wardrive.js:1678` - **Context**: GPS data is stale and needs refresh (used in both auto and manual ping modes) - **Minimum Visibility**: 500ms minimum enforced --- -### 4. Countdown Timer Messages +### 7. Countdown Timer Messages These messages use a hybrid approach: **first display respects 500ms minimum**, then updates occur immediately every second. @@ -216,13 +340,13 @@ These messages use a hybrid approach: **first display respects 500ms minimum**, --- -### 5. API and Map Update Messages +### 8. API and Map Update Messages #### Posting to API - **Message**: `"Posting to API"` - **Color**: Sky blue (info) - **Used in**: `postApiAndRefreshMap()` -- **Source**: `content/wardrive.js:1055` +- **Source**: `content/wardrive.js:1167` - **Context**: After RX listening window, posting ping data to MeshMapper API - **Timing**: Visible during API POST operation (3-second hidden delay + API call time, typically ~3.5-4.5s total) - **Minimum Visibility**: 500ms minimum enforced (naturally ~4s due to 3s delay + API timing) @@ -232,19 +356,20 @@ These messages use a hybrid approach: **first display respects 500ms minimum**, - **Message**: `"Idle"` - **Color**: Slate (idle) - **Used in**: `postApiAndRefreshMap()` -- **Source**: `content/wardrive.js:1091` -- **Context**: Manual mode, after API post completes +- **Source**: `content/wardrive.js:1203` +- **Context**: Manual mode after API post completes - **Minimum Visibility**: 500ms minimum enforced +- **Note**: No longer shown after initial connection; "Connected" status is displayed instead and persists until user action --- -### 6. Auto Mode Messages +### 9. Auto Mode Messages #### Auto mode stopped - **Message**: `"Auto mode stopped"` - **Color**: Slate (idle) - **Used in**: `disconnect()` (event handler for stopping auto mode) -- **Source**: `content/wardrive.js:2070` +- **Source**: `content/wardrive.js:2247` - **Context**: User clicks "Stop Auto Ping" button - **Minimum Visibility**: 500ms minimum enforced @@ -252,7 +377,7 @@ These messages use a hybrid approach: **first display respects 500ms minimum**, - **Message**: `"Lost focus, auto mode stopped"` - **Color**: Amber (warning) - **Used in**: `disconnect()` (page visibility handler) -- **Source**: `content/wardrive.js:2032` +- **Source**: `content/wardrive.js:2209` - **Context**: Browser tab hidden while auto mode running - **Minimum Visibility**: 500ms minimum enforced @@ -260,7 +385,7 @@ These messages use a hybrid approach: **first display respects 500ms minimum**, - **Message**: `"Wait Xs before toggling auto mode"` (X is dynamic countdown) - **Color**: Amber (warning) - **Used in**: `stopAutoPing()`, `startAutoPing()` -- **Source**: `content/wardrive.js:1816`, `content/wardrive.js:1876` +- **Source**: `content/wardrive.js:1928`, `content/wardrive.js:1988` - **Context**: User attempts to toggle auto mode during cooldown period - **Minimum Visibility**: 500ms minimum enforced @@ -323,10 +448,13 @@ Result: "Message A" (visible 500ms) → "Message C" ## Summary -**Total Status Messages**: 25 unique message patterns +**Total Status Messages**: 38 unique message patterns - **Connection**: 7 messages +- **Capacity Check**: 7 messages (1 deprecated, includes new "Acquired wardriving slot" and "Error: Posting to API (Revoked)") +- **Channel Setup**: 4 messages (new section for channel lookup and creation) +- **GPS Initialization**: 1 message (new "Priming GPS") - **Ping Operation**: 6 messages (consolidated "Ping sent" for both manual and auto) -- **GPS**: 2 messages +- **GPS Status**: 2 messages - **Countdown Timers**: 6 message patterns (with dynamic countdown values) - **API/Map**: 2 messages - **Auto Mode**: 3 messages @@ -341,3 +469,4 @@ Result: "Message A" (visible 500ms) → "Message C" - Consistent "X failed" format for error messages - Consistent tone (direct, technical) - removed "Please" from wait messages - Proper compound words ("geofenced" not "geo fenced") +- Correct spelling: "Acquired" (not "Aquired")