@@ -126,8 +126,8 @@ const API_TX_FLUSH_DELAY_MS = 3000; // Flush 3 seconds after TX ping
126126
127127// MeshMapper API Configuration
128128const MESHMAPPER_API_URL = "https://yow.meshmapper.net/wardriving-api.php" ;
129- const MESHMAPPER_CAPACITY_CHECK_URL = "https://yow.meshmapper.net/capacitycheck.php" ;
130129const GEO_AUTH_STATUS_URL = "https://meshmapper.net/wardrive-api.php/status" ; // Geo-auth zone status endpoint
130+ const GEO_AUTH_URL = "https://meshmapper.net/wardrive-api.php/auth" ; // Geo-auth connect/disconnect endpoint
131131const MESHMAPPER_API_KEY = "59C7754DABDF5C11CA5F5D8368F89" ;
132132const MESHMAPPER_DEFAULT_WHO = "GOME-WarDriver" ; // Default identifier
133133const MESHMAPPER_RX_LOG_API_URL = "https://yow.meshmapper.net/wardriving-api.php" ;
@@ -141,11 +141,19 @@ const WARDIVE_IATA_CODE = "YOW";
141141// For DEV builds: Contains "DEV-<EPOCH>" format (e.g., "DEV-1734652800")
142142const APP_VERSION = "UNKNOWN" ; // Placeholder - replaced during build
143143
144- // ---- Capacity Check Reason Messages ----
144+ // ---- Auth Reason Messages ----
145145// Maps API reason codes to user-facing error messages
146146const REASON_MESSAGES = {
147147 outofdate : "App out of date, please update" ,
148- // Future reasons can be added here
148+ unknown_device : "Unknown device - advertise on mesh first" ,
149+ outside_zone : "Outside zone - cannot connect" ,
150+ zone_disabled : "Zone is disabled" ,
151+ zone_full : "TX slots full - RX only" ,
152+ bad_key : "Invalid API key" ,
153+ gps_stale : "GPS data too old - try again" ,
154+ gps_inaccurate : "GPS accuracy too low - try again" ,
155+ bad_session : "Invalid session" ,
156+ session_expired : "Session expired - reconnect" ,
149157} ;
150158
151159// ---- UI helpers ----
@@ -269,16 +277,19 @@ const state = {
269277 pausedAutoTimerRemainingMs : null , // Remaining time when auto ping timer was paused by manual ping
270278 lastSuccessfulPingLocation : null , // { lat, lon } of the last successful ping (Mesh + API)
271279 capturedPingCoords : null , // { lat, lon, accuracy } captured at ping time, used for API post after 7s delay
272- devicePublicKey : null , // Hex string of device's public key (used for capacity check )
280+ devicePublicKey : null , // Hex string of device's public key (used for auth )
273281 deviceModel : null , // Manufacturer/model string exposed by companion
274282 autoPowerSet : false , // Whether power was automatically set based on device model
275283 lastNoiseFloor : null , // Most recent noise floor read from companion (dBm) or 'ERR'
276284 noiseFloorUpdateTimer : null , // Timer for periodic noise floor updates (5s interval)
277285 deviceName : null ,
278- wardriveSessionId : null , // Session ID from capacity check API (used for all MeshMapper API posts)
286+ wardriveSessionId : null , // Session ID from /auth API (used for all MeshMapper API posts)
279287 debugMode : false , // Whether debug mode is enabled by MeshMapper API
288+ txAllowed : false , // Whether TX wardriving is permitted (from /auth response)
289+ rxAllowed : false , // Whether RX wardriving is permitted (from /auth response)
290+ sessionExpiresAt : null , // Unix timestamp when session expires (for heartbeat scheduling)
280291 tempTxRepeaterData : null , // Temporary storage for TX repeater debug data
281- disconnectReason : null , // Tracks the reason for disconnection (e.g., "app_down", "capacity_full ", "public_key_error ", "channel_setup_error", "ble_disconnect_error", "session_id_error", "normal", or API reason codes like "outofdate")
292+ disconnectReason : null , // Tracks the reason for disconnection (e.g., "app_down", "unknown_device ", "outside_zone", "zone_disabled ", "channel_setup_error", "ble_disconnect_error", "session_id_error", "normal", or API reason codes like "outofdate")
282293 channelSetupErrorMessage : null , // Error message from channel setup failure
283294 bleDisconnectErrorMessage : null , // Error message from BLE disconnect failure
284295 pendingApiPosts : [ ] , // Array of pending background API post promises
@@ -604,16 +615,17 @@ function updateControlsForCooldown() {
604615 const connected = ! ! state . connection ;
605616 const inCooldown = isInCooldown ( ) ;
606617 const powerSelected = getCurrentPowerSetting ( ) !== "" ;
607- debugLog ( `[UI] updateControlsForCooldown: connected=${ connected } , inCooldown=${ inCooldown } , pingInProgress=${ state . pingInProgress } , txRxAutoRunning=${ state . txRxAutoRunning } , rxAutoRunning=${ state . rxAutoRunning } , powerSelected=${ powerSelected } ` ) ;
618+ debugLog ( `[UI] updateControlsForCooldown: connected=${ connected } , inCooldown=${ inCooldown } , pingInProgress=${ state . pingInProgress } , txRxAutoRunning=${ state . txRxAutoRunning } , rxAutoRunning=${ state . rxAutoRunning } , powerSelected=${ powerSelected } , txAllowed= ${ state . txAllowed } , rxAllowed= ${ state . rxAllowed } ` ) ;
608619
609- // TX Ping button - disabled during cooldown, ping in progress, OR when no power selected
610- txPingBtn . disabled = ! connected || inCooldown || state . pingInProgress || ! powerSelected ;
620+ // TX Ping button - requires TX permission, disabled during cooldown, ping in progress, OR when no power selected
621+ txPingBtn . disabled = ! connected || ! state . txAllowed || inCooldown || state . pingInProgress || ! powerSelected ;
611622
612- // TX/RX Auto button - disabled during cooldown, ping in progress, when RX Auto running, OR when no power selected
613- txRxAutoBtn . disabled = ! connected || inCooldown || state . pingInProgress || state . rxAutoRunning || ! powerSelected ;
623+ // TX/RX Auto button - requires TX permission, disabled during cooldown, ping in progress, when RX Auto running, OR when no power selected
624+ txRxAutoBtn . disabled = ! connected || ! state . txAllowed || inCooldown || state . pingInProgress || state . rxAutoRunning || ! powerSelected ;
614625
615- // RX Auto button - permanently disabled (backend API not ready)
616- rxAutoBtn . disabled = true ;
626+ // RX Auto button - enabled when connected with RX permission (including RX-only mode)
627+ // Disabled during TX/RX Auto mode (can't run both), and requires power selected
628+ rxAutoBtn . disabled = ! connected || ! state . rxAllowed || state . txRxAutoRunning || ! powerSelected ;
617629}
618630
619631/**
@@ -1936,14 +1948,15 @@ async function checkZoneStatus(coords) {
19361948}
19371949
19381950/**
1939- * Check capacity / slot availability with MeshMapper API
1940- * @param {string } reason - Either "connect" (acquire slot) or "disconnect" (release slot)
1951+ * Request authentication with MeshMapper geo-auth API
1952+ * Handles both connect (acquire session) and disconnect (release session)
1953+ * @param {string } reason - Either "connect" (acquire session) or "disconnect" (release session)
19411954 * @returns {Promise<boolean> } True if allowed to continue, false otherwise
19421955 */
1943- async function checkCapacity ( reason ) {
1956+ async function requestAuth ( reason ) {
19441957 // Validate public key exists
19451958 if ( ! state . devicePublicKey ) {
1946- debugError ( "[CAPACITY] checkCapacity called but no public key stored" ) ;
1959+ debugError ( "[AUTH] requestAuth called but no public key stored" ) ;
19471960 return reason === "connect" ? false : true ; // Fail closed on connect, allow disconnect
19481961 }
19491962
@@ -1953,93 +1966,146 @@ async function checkCapacity(reason) {
19531966 }
19541967
19551968 try {
1969+ // Build base payload
19561970 const payload = {
19571971 key : MESHMAPPER_API_KEY ,
19581972 public_key : state . devicePublicKey ,
1959- ver : APP_VERSION ,
1960- who : getDeviceIdentifier ( ) ,
1961- ver : APP_VERSION ,
19621973 reason : reason
19631974 } ;
19641975
1965- debugLog ( `[CAPACITY] Checking capacity: reason=${ reason } , public_key=${ state . devicePublicKey . substring ( 0 , 16 ) } ..., who=${ payload . who } ` ) ;
1976+ // For connect: add device metadata and GPS coords
1977+ if ( reason === "connect" ) {
1978+ // Acquire fresh GPS for auth
1979+ debugLog ( "[AUTH] Acquiring fresh GPS for auth request" ) ;
1980+ const coords = await getValidGpsForZoneCheck ( ) ;
1981+
1982+ if ( ! coords ) {
1983+ debugError ( "[AUTH] Failed to acquire GPS for auth" ) ;
1984+ state . disconnectReason = "gps_unavailable" ;
1985+ return false ;
1986+ }
1987+
1988+ // Add device metadata (bound to session at auth time)
1989+ payload . who = getDeviceIdentifier ( ) ;
1990+ payload . ver = APP_VERSION ;
1991+ payload . power = getCurrentPowerSetting ( ) ;
1992+ payload . iata = state . currentZone ?. code || WARDIVE_IATA_CODE ;
1993+ payload . model = state . deviceModel || "Unknown" ;
1994+
1995+ // Add GPS coords (use lng for API, internally we use lon)
1996+ payload . coords = {
1997+ lat : coords . lat ,
1998+ lng : coords . lon , // Convert lon → lng for API
1999+ accuracy_m : coords . accuracy_m ,
2000+ timestamp : coords . timestamp
2001+ } ;
2002+
2003+ debugLog ( `[AUTH] Connect request: public_key=${ state . devicePublicKey . substring ( 0 , 16 ) } ..., who=${ payload . who } , iata=${ payload . iata } ` ) ;
2004+ } else {
2005+ // For disconnect: add session_id
2006+ payload . session_id = state . wardriveSessionId ;
2007+ debugLog ( `[AUTH] Disconnect request: public_key=${ state . devicePublicKey . substring ( 0 , 16 ) } ..., session_id=${ payload . session_id } ` ) ;
2008+ }
19662009
1967- const response = await fetch ( MESHMAPPER_CAPACITY_CHECK_URL , {
2010+ const response = await fetch ( GEO_AUTH_URL , {
19682011 method : "POST" ,
19692012 headers : { "Content-Type" : "application/json" } ,
19702013 body : JSON . stringify ( payload )
19712014 } ) ;
19722015
2016+ // Handle HTTP-level errors
19732017 if ( ! response . ok ) {
1974- debugError ( `[CAPACITY] Capacity check API returned error status ${ response . status } ` ) ;
2018+ debugError ( `[AUTH] API returned error status ${ response . status } ` ) ;
19752019 // Fail closed on network errors for connect
19762020 if ( reason === "connect" ) {
1977- debugError ( "[CAPACITY ] Failing closed (denying connection) due to API error" ) ;
1978- state . disconnectReason = "app_down" ; // Track disconnect reason
2021+ debugError ( "[AUTH ] Failing closed (denying connection) due to API error" ) ;
2022+ state . disconnectReason = "app_down" ;
19792023 return false ;
19802024 }
19812025 return true ; // Always allow disconnect to proceed
19822026 }
19832027
19842028 const data = await response . json ( ) ;
1985- debugLog ( `[CAPACITY] Capacity check response: allowed=${ data . allowed } , session_id=${ data . session_id || 'missing' } , debug_mode=${ data . debug_mode || 'not set' } , reason=${ data . reason || 'none' } ` ) ;
1986-
1987- // Handle capacity full vs. allowed cases separately
1988- if ( data . allowed === false && reason === "connect" ) {
1989- // Check if a reason code is provided
1990- if ( data . reason ) {
1991- debugLog ( `[CAPACITY] API returned reason code: ${ data . reason } ` ) ;
1992- state . disconnectReason = data . reason ; // Store the reason code directly
1993- } else {
1994- state . disconnectReason = "capacity_full" ; // Default to capacity_full
2029+ debugLog ( `[AUTH] Response: success=${ data . success } , tx_allowed=${ data . tx_allowed } , rx_allowed=${ data . rx_allowed } , session_id=${ data . session_id || 'none' } , reason=${ data . reason || 'none' } ` ) ;
2030+
2031+ // Handle connect response
2032+ if ( reason === "connect" ) {
2033+ // Check for full denial
2034+ if ( data . success === false ) {
2035+ debugLog ( `[AUTH] Connect denied: ${ data . reason } - ${ data . message || '' } ` ) ;
2036+ state . disconnectReason = data . reason || "auth_denied" ;
2037+ return false ;
19952038 }
1996- return false ;
1997- }
1998-
1999- // For connect requests, validate session_id and check debug_mode
2000- if ( reason === "connect" && data . allowed === true ) {
2039+
2040+ // Success - store session info
20012041 if ( ! data . session_id ) {
2002- debugError ( "[CAPACITY] Capacity check returned allowed =true but session_id is missing" ) ;
2003- state . disconnectReason = "session_id_error" ; // Track disconnect reason
2042+ debugError ( "[AUTH] Auth returned success =true but session_id is missing" ) ;
2043+ state . disconnectReason = "session_id_error" ;
20042044 return false ;
20052045 }
20062046
2007- // Store the session_id for use in MeshMapper API posts
2047+ // Store session data
20082048 state . wardriveSessionId = data . session_id ;
2009- debugLog ( `[CAPACITY] Wardrive session ID received and stored: ${ state . wardriveSessionId } ` ) ;
2049+ state . txAllowed = data . tx_allowed === true ;
2050+ state . rxAllowed = data . rx_allowed === true ;
2051+ state . sessionExpiresAt = data . expires_at || null ;
2052+
2053+ debugLog ( `[AUTH] Session acquired: id=${ state . wardriveSessionId } , tx=${ state . txAllowed } , rx=${ state . rxAllowed } , expires=${ state . sessionExpiresAt } ` ) ;
2054+
2055+ // Check for RX-only scenario (zone_full)
2056+ if ( ! state . txAllowed && state . rxAllowed ) {
2057+ debugLog ( `[AUTH] RX-only mode: TX slots full, reason=${ data . reason } ` ) ;
2058+ // Don't set disconnectReason - this is a partial success
2059+ }
20102060
20112061 // Check for debug_mode flag (optional field)
20122062 if ( data . debug_mode === 1 ) {
20132063 state . debugMode = true ;
2014- debugLog ( `[CAPACITY ] 🐛 DEBUG MODE ENABLED by API` ) ;
2064+ debugLog ( `[AUTH ] 🐛 DEBUG MODE ENABLED by API` ) ;
20152065 } else {
20162066 state . debugMode = false ;
2017- debugLog ( `[CAPACITY] Debug mode NOT enabled` ) ;
20182067 }
2068+
2069+ return true ; // Success (full or RX-only)
20192070 }
20202071
2021- // For disconnect requests, clear the session_id and debug mode
2072+ // Handle disconnect response
20222073 if ( reason === "disconnect" ) {
2023- if ( state . wardriveSessionId ) {
2024- debugLog ( `[CAPACITY] Clearing wardrive session ID on disconnect: ${ state . wardriveSessionId } ` ) ;
2025- state . wardriveSessionId = null ;
2026- }
2074+ // Clear session state regardless of server response
2075+ debugLog ( `[AUTH] Clearing session state on disconnect` ) ;
2076+ state . wardriveSessionId = null ;
2077+ state . txAllowed = false ;
2078+ state . rxAllowed = false ;
2079+ state . sessionExpiresAt = null ;
20272080 state . debugMode = false ;
2028- debugLog ( `[CAPACITY] Debug mode cleared on disconnect` ) ;
2081+
2082+ if ( data . success === true && data . disconnected === true ) {
2083+ debugLog ( `[AUTH] Disconnect confirmed by server` ) ;
2084+ } else if ( data . success === false ) {
2085+ debugWarn ( `[AUTH] Server reported disconnect error: ${ data . reason } - ${ data . message || '' } ` ) ;
2086+ // Don't fail - we still clean up locally
2087+ }
2088+
2089+ return true ; // Always return true for disconnect
20292090 }
2030-
2031- return data . allowed === true ;
20322091
20332092 } catch ( error ) {
2034- debugError ( `[CAPACITY] Capacity check failed: ${ error . message } ` ) ;
2093+ debugError ( `[AUTH] Request failed: ${ error . message } ` ) ;
20352094
20362095 // Fail closed on network errors for connect
20372096 if ( reason === "connect" ) {
2038- debugError ( "[CAPACITY ] Failing closed (denying connection) due to network error" ) ;
2039- state . disconnectReason = "app_down" ; // Track disconnect reason
2097+ debugError ( "[AUTH ] Failing closed (denying connection) due to network error" ) ;
2098+ state . disconnectReason = "app_down" ;
20402099 return false ;
20412100 }
20422101
2102+ // For disconnect, clear state even on error
2103+ state . wardriveSessionId = null ;
2104+ state . txAllowed = false ;
2105+ state . rxAllowed = false ;
2106+ state . sessionExpiresAt = null ;
2107+ state . debugMode = false ;
2108+
20432109 return true ; // Always allow disconnect to proceed
20442110 }
20452111}
@@ -5226,22 +5292,29 @@ async function connect() {
52265292 debugLog ( "[BLE] Device time sync not available or failed" ) ;
52275293 }
52285294 try {
5229- // Check capacity immediately after time sync, before channel setup and GPS init
5230- const allowed = await checkCapacity ( "connect" ) ;
5295+ // Request auth immediately after time sync, before channel setup and GPS init
5296+ // Note: requestAuth acquires fresh GPS internally
5297+ const allowed = await requestAuth ( "connect" ) ;
52315298 if ( ! allowed ) {
5232- debugWarn ( "[CAPACITY] Capacity check denied, disconnecting" ) ;
5233- // disconnectReason already set by checkCapacity ()
5299+ debugWarn ( "[AUTH] Auth request denied, disconnecting" ) ;
5300+ // disconnectReason already set by requestAuth ()
52345301 // Status message will be set by disconnected event handler based on disconnectReason
52355302 // Disconnect after a brief delay to ensure "Acquiring wardriving slot" is visible
52365303 setTimeout ( ( ) => {
5237- disconnect ( ) . catch ( err => debugError ( `[BLE] Disconnect after capacity denial failed: ${ err . message } ` ) ) ;
5304+ disconnect ( ) . catch ( err => debugError ( `[BLE] Disconnect after auth denial failed: ${ err . message } ` ) ) ;
52385305 } , 1500 ) ;
52395306 return ;
52405307 }
52415308
5242- // Capacity check passed
5243- setDynamicStatus ( "Acquired wardriving slot" , STATUS_COLORS . success ) ;
5244- debugLog ( "[BLE] Wardriving slot acquired successfully" ) ;
5309+ // Auth passed - check if full access or RX-only
5310+ if ( state . txAllowed && state . rxAllowed ) {
5311+ setDynamicStatus ( "Acquired wardriving slot" , STATUS_COLORS . success ) ;
5312+ debugLog ( "[AUTH] Full access granted (TX + RX)" ) ;
5313+ } else if ( state . rxAllowed ) {
5314+ setDynamicStatus ( "TX slots full - RX only" , STATUS_COLORS . warning ) ;
5315+ debugLog ( "[AUTH] RX-only access granted (TX slots full)" ) ;
5316+ }
5317+ debugLog ( `[AUTH] Session acquired: tx=${ state . txAllowed } , rx=${ state . rxAllowed } ` ) ;
52455318
52465319 // Proceed with channel setup and GPS initialization
52475320 await ensureChannel ( ) ;
@@ -5250,13 +5323,17 @@ async function connect() {
52505323 startUnifiedRxListening ( ) ;
52515324 debugLog ( "[BLE] Unified RX listener started on connect" ) ;
52525325
5253- // GPS initialization
5254- debugLog ( "[BLE] Starting GPS initialization" ) ;
5326+ // GPS initialization (primeGpsOnce for watch mode)
5327+ // Note: Fresh GPS was already acquired by requestAuth, this starts continuous watching
5328+ debugLog ( "[BLE] Starting GPS watch mode" ) ;
52555329 await primeGpsOnce ( ) ;
52565330
5257- // Connection complete, show Connected status in connection bar
5258- // Note: Zone validation will happen server-side via /auth endpoint (Phase 4.2)
5259- setConnStatus ( "Connected" , STATUS_COLORS . success ) ;
5331+ // Connection complete - show status based on TX+RX vs RX-only
5332+ if ( state . txAllowed && state . rxAllowed ) {
5333+ setConnStatus ( "Connected" , STATUS_COLORS . success ) ;
5334+ } else if ( state . rxAllowed ) {
5335+ setConnStatus ( "Connected (RX Only)" , STATUS_COLORS . warning ) ;
5336+ }
52605337
52615338 // If device is unknown and power not selected, show warning message
52625339 if ( ! state . autoPowerSet && ! getCurrentPowerSetting ( ) ) {
@@ -5385,6 +5462,9 @@ async function connect() {
53855462 state . channel = null ;
53865463 state . devicePublicKey = null ; // Clear public key
53875464 state . wardriveSessionId = null ; // Clear wardrive session ID
5465+ state . txAllowed = false ; // Clear TX permission
5466+ state . rxAllowed = false ; // Clear RX permission
5467+ state . sessionExpiresAt = null ; // Clear session expiration
53885468 state . debugMode = false ; // Clear debug mode
53895469 state . tempTxRepeaterData = null ; // Clear temp TX data
53905470 state . disconnectReason = null ; // Reset disconnect reason
@@ -5498,14 +5578,14 @@ async function disconnect() {
54985578 }
54995579 stopFlushTimers ( ) ;
55005580
5501- // 3. THEN release capacity slot if we have a public key
5502- if ( state . devicePublicKey ) {
5581+ // 3. THEN release session via auth API if we have a public key
5582+ if ( state . devicePublicKey && state . wardriveSessionId ) {
55035583 try {
5504- debugLog ( "[BLE ] Releasing capacity slot " ) ;
5505- await checkCapacity ( "disconnect" ) ;
5584+ debugLog ( "[AUTH ] Releasing session via /auth disconnect " ) ;
5585+ await requestAuth ( "disconnect" ) ;
55065586 } catch ( e ) {
5507- debugWarn ( `[CAPACITY ] Failed to release capacity slot : ${ e . message } ` ) ;
5508- // Don't fail disconnect if capacity release fails
5587+ debugWarn ( `[AUTH ] Failed to release session : ${ e . message } ` ) ;
5588+ // Don't fail disconnect if auth release fails
55095589 }
55105590 }
55115591
0 commit comments