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")