@@ -69,30 +69,30 @@ const FALLBACK_SUBNETS = ['192.168.1', '192.168.0'];
6969 * Returns the first reachable subnet base, or null if none respond.
7070 * Uses a short timeout so we bail fast when on cellular.
7171 */
72- async function findReachableSubnet ( subnets : string [ ] ) : Promise < string | null > {
72+ async function findReachableSubnet ( subnets : string [ ] , log : ( msg : string ) => void ) : Promise < string | null > {
7373 const GATEWAY_TIMEOUT_MS = 800 ;
7474 const results = await Promise . all (
7575 subnets . map ( async ( base ) => {
7676 const gateway = `${ base } .1` ;
77- // Try any HTTP connection to the gateway — we don't care about the response,
78- // just that something is listening on the local network.
7977 const controller = new AbortController ( ) ;
8078 const timer = setTimeout ( ( ) => controller . abort ( ) , GATEWAY_TIMEOUT_MS ) ;
8179 try {
8280 await fetch ( `http://${ gateway } :80/` , { signal : controller . signal } ) ; // NOSONAR — LAN gateway probe
8381 clearTimeout ( timer ) ;
82+ log ( `Gateway ${ gateway } :80 responded` ) ;
8483 return base ;
8584 } catch {
8685 clearTimeout ( timer ) ;
87- // Also try the Ollama port since routers may not serve HTTP on :80
8886 const controller2 = new AbortController ( ) ;
8987 const timer2 = setTimeout ( ( ) => controller2 . abort ( ) , GATEWAY_TIMEOUT_MS ) ;
9088 try {
9189 await fetch ( `http://${ gateway } :11434/` , { signal : controller2 . signal } ) ; // NOSONAR — LAN Ollama probe
9290 clearTimeout ( timer2 ) ;
91+ log ( `Gateway ${ gateway } :11434 responded` ) ;
9392 return base ;
9493 } catch {
9594 clearTimeout ( timer2 ) ;
95+ log ( `Gateway ${ gateway } did not respond on :80 or :11434` ) ;
9696 return null ;
9797 }
9898 }
@@ -107,53 +107,64 @@ async function findReachableSubnet(subnets: string[]): Promise<string | null> {
107107 * Throws with a human-readable message if setup fails (no WiFi IP, non-private network).
108108 * Errors during probing are swallowed — only setup errors propagate.
109109 */
110- export async function discoverLANServers ( ) : Promise < DiscoveredServer [ ] > {
110+ export async function discoverLANServers ( onLog ?: ( msg : string ) => void ) : Promise < DiscoveredServer [ ] > {
111+ const log = ( msg : string ) => {
112+ logger . warn ( '[Discovery]' , msg ) ;
113+ onLog ?.( msg ) ;
114+ } ;
115+
111116 let runningOnEmulator : boolean ;
112117 try {
113118 runningOnEmulator = await isEmulator ( ) ;
114119 } catch ( err ) {
115- logger . warn ( '[Discovery] isEmulator() threw:' , ( err as Error ) . message ) ;
120+ log ( ` isEmulator() threw: ${ ( err as Error ) . message } — assuming not emulator` ) ;
116121 runningOnEmulator = false ;
117122 }
118123 if ( runningOnEmulator ) {
119- logger . warn ( '[Discovery] Running on emulator — skipping LAN scan (emulator network stack cannot handle concurrent probes)') ;
124+ log ( ' Running on emulator — skipping scan (emulator network stack cannot handle concurrent probes)') ;
120125 return [ ] ;
121126 }
122127
128+ log ( 'Not an emulator — proceeding' ) ;
129+
123130 let ip : string | null ;
124131 try {
125132 ip = await getIpAddress ( ) ;
126133 } catch ( err ) {
127- logger . warn ( '[Discovery] getIpAddress threw:' , ( err as Error ) . message ) ;
134+ log ( ` getIpAddress() threw: ${ ( err as Error ) . message } ` ) ;
128135 ip = null ;
129136 }
130137
138+ const ipv6 = ip ? isIPv6 ( ip ) : false ;
139+ const privateV4 = ip ? isPrivateIPv4 ( ip ) : false ;
140+ log ( `Device IP: ${ ip ?? 'null' } | IPv6: ${ ipv6 } | privateIPv4: ${ privateV4 } ` ) ;
141+
131142 let subnetsToScan : string [ ] ;
132143
133144 if ( ! ip || ip === '0.0.0.0' || ip === '127.0.0.1' ) {
134- logger . warn ( '[Discovery] No WiFi IP (got:' , ip || 'null' , ' ) — skipping LAN scan' ) ;
145+ log ( ` No usable IP (got: ${ ip ?? 'null' } ) — skipping scan` ) ;
135146 return [ ] ;
136- } else if ( isIPv6 ( ip ) ) {
137- // IPv6 primary address — could be WiFi or cellular, but we can't scan IPv4 subnets
138- // without a real IP. Quick-probe the two most common gateways before committing to a
139- // full subnet scan so we don't waste time on cellular.
140- logger . warn ( '[Discovery] Got IPv6 address:' , ip , '— quick-probing common gateways' ) ;
141- const reachableSubnet = await findReachableSubnet ( FALLBACK_SUBNETS ) ;
142- if ( ! reachableSubnet ) {
143- logger . warn ( '[Discovery] No gateway responded — likely not on WiFi, skipping scan ') ;
144- return [ ] ;
147+ } else if ( ipv6 ) {
148+ log ( ` IPv6 address detected — probing gateways on fallback subnets: ${ FALLBACK_SUBNETS . join ( ', ' ) } ` ) ;
149+ const reachableSubnet = await findReachableSubnet ( FALLBACK_SUBNETS , log ) ;
150+ if ( reachableSubnet ) {
151+ log ( `Gateway responded on subnet ${ reachableSubnet } — scanning that subnet only` ) ;
152+ subnetsToScan = [ reachableSubnet ] ;
153+ } else {
154+ log ( ' No gateway responded — scanning all fallback subnets anyway (device may still be on WiFi) ') ;
155+ subnetsToScan = FALLBACK_SUBNETS ;
145156 }
146- subnetsToScan = [ reachableSubnet ] ;
147157 } else {
148158 const base = subnetBase ( ip ) ;
149159 if ( ! base ) {
150- logger . warn ( '[Discovery] IP is not on a private network:' , ip , ' — skipping LAN scan' ) ;
160+ log ( ` IP ${ ip } is not on a private network — skipping scan` ) ;
151161 return [ ] ;
152162 }
163+ log ( `IPv4 private address — subnet base: ${ base } ` ) ;
153164 subnetsToScan = [ base ] ;
154165 }
155166
156- logger . log ( '[Discovery] Scanning subnets:' , subnetsToScan . map ( s => `${ s } .0/24` ) . join ( ', ' ) ) ;
167+ log ( ` Scanning ${ subnetsToScan . length } subnet(s): ${ subnetsToScan . map ( s => `${ s } .0/24` ) . join ( ', ' ) } | ${ subnetsToScan . length * 254 * PROVIDERS . length } total probes | batch size: ${ BATCH_SIZE } | timeout: ${ TIMEOUT_MS } ms` ) ;
157168
158169 try {
159170 const discovered : DiscoveredServer [ ] = [ ] ;
@@ -164,26 +175,27 @@ export async function discoverLANServers(): Promise<DiscoveredServer[]> {
164175 const endpoint = `http://${ target } :${ provider . port } ` ; // NOSONAR — LAN endpoint
165176 if ( ! seenEndpoints . has ( endpoint ) ) {
166177 seenEndpoints . add ( endpoint ) ;
167- logger . log ( `[Discovery] Found ${ provider . name } at ${ target } :${ provider . port } ` ) ;
178+ log ( `Found ${ provider . name } at ${ target } :${ provider . port } ` ) ;
168179 discovered . push ( { endpoint, type : provider . type , name : `${ provider . name } (${ target } )` } ) ;
169180 }
170181 } ;
171182
172- // Scan all subnets in parallel — each subnet is independent
173183 await Promise . all ( subnetsToScan . map ( async ( base ) => {
174184 for ( const provider of PROVIDERS ) {
185+ log ( `Probing ${ base } .1-254 for ${ provider . name } on port ${ provider . port } ...` ) ;
175186 const tasks = Array . from ( { length : 254 } , ( _ , i ) => {
176187 const target = `${ base } .${ i + 1 } ` ;
177188 return ( ) => probe ( target , provider . port , provider . probePath ) . then ( recordIfFound ( target , provider ) ) ;
178189 } ) ;
179190 await runBatch ( tasks ) ;
191+ log ( `Done probing ${ base } .x for ${ provider . name } ` ) ;
180192 }
181193 } ) ) ;
182194
183- logger . log ( '[Discovery] Scan complete, found:' , discovered . length , 'servers' ) ;
195+ log ( ` Scan complete — found ${ discovered . length } server(s)` ) ;
184196 return discovered ;
185197 } catch ( error ) {
186- logger . warn ( '[Discovery] Scan error during probing:' , error ) ;
198+ log ( ` Scan error: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
187199 return [ ] ;
188200 }
189201}
0 commit comments